elasticsearch-model-7.2.1/0000755000004100000410000000000014217570270015466 5ustar www-datawww-dataelasticsearch-model-7.2.1/README.md0000644000004100000410000006216014217570270016752 0ustar www-datawww-data# Elasticsearch::Model The `elasticsearch-model` library builds on top of the the [`elasticsearch`](https://github.com/elastic/elasticsearch-ruby) library. It aims to simplify integration of Ruby classes ("models"), commonly found e.g. in [Ruby on Rails](http://rubyonrails.org) applications, with the [Elasticsearch](https://www.elastic.co) search and analytics engine. ## Compatibility This library is compatible with Ruby 2.4 and higher. The library version numbers follow the Elasticsearch major versions. The `main` branch is compatible with the latest Elasticsearch stack stable release. | Rubygem | | Elasticsearch | |:-------------:|:-:| :-----------: | | 0.1 | → | 1.x | | 2.x | → | 2.x | | 5.x | → | 5.x | | 6.x | → | 6.x | | main | → | 7.x | ## Installation Install the package from [Rubygems](https://rubygems.org): gem install elasticsearch-model To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://bundler.io): gem 'elasticsearch-model', git: 'git://github.com/elastic/elasticsearch-rails.git', branch: '5.x' or install it from a source code checkout: git clone https://github.com/elastic/elasticsearch-rails.git cd elasticsearch-rails/elasticsearch-model bundle install rake install ## Usage Let's suppose you have an `Article` model: ```ruby require 'active_record' ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" ) ActiveRecord::Schema.define(version: 1) { create_table(:articles) { |t| t.string :title } } class Article < ActiveRecord::Base; end Article.create title: 'Quick brown fox' Article.create title: 'Fast black dogs' Article.create title: 'Swift green frogs' ``` ### Setup To add the Elasticsearch integration for this model, require `elasticsearch/model` and include the main module in your class: ```ruby require 'elasticsearch/model' class Article < ActiveRecord::Base include Elasticsearch::Model end ``` This will extend the model with functionality related to Elasticsearch. #### Feature Extraction Pattern Instead of including the `Elasticsearch::Model` module directly in your model, you can include it in a "concern" or "trait" module, which is quite common pattern in Rails applications, using e.g. `ActiveSupport::Concern` as the instrumentation: ```ruby # In: app/models/concerns/searchable.rb # module Searchable extend ActiveSupport::Concern included do include Elasticsearch::Model mapping do # ... end def self.search(query) # ... end end end # In: app/models/article.rb # class Article include Searchable end ``` #### The `__elasticsearch__` Proxy The `Elasticsearch::Model` module contains a big amount of class and instance methods to provide all its functionality. To prevent polluting your model namespace, this functionality is primarily available via the `__elasticsearch__` class and instance level proxy methods; see the `Elasticsearch::Model::Proxy` class documentation for technical information. The module will include important methods, such as `search`, into the class or module only when they haven't been defined already. Following two calls are thus functionally equivalent: ```ruby Article.__elasticsearch__.search 'fox' Article.search 'fox' ``` See the `Elasticsearch::Model` module documentation for technical information. ### The Elasticsearch client The module will set up a [client](https://github.com/elastic/elasticsearch-ruby/tree/main/elasticsearch), connected to `localhost:9200`, by default. You can access and use it as any other `Elasticsearch::Client`: ```ruby Article.__elasticsearch__.client.cluster.health # => { "cluster_name"=>"elasticsearch", "status"=>"yellow", ... } ``` To use a client with different configuration, just set up a client for the model: ```ruby Article.__elasticsearch__.client = Elasticsearch::Client.new host: 'api.server.org' ``` Or configure the client for all models: ```ruby Elasticsearch::Model.client = Elasticsearch::Client.new log: true ``` You might want to do this during your application bootstrap process, e.g. in a Rails initializer. Please refer to the [`elasticsearch-transport`](https://github.com/elastic/elasticsearch-ruby/tree/main/elasticsearch-transport) library documentation for all the configuration options, and to the [`elasticsearch-api`](http://rubydoc.info/gems/elasticsearch-api) library documentation for information about the Ruby client API. ### Importing the data The first thing you'll want to do is import your data into the index: ```ruby Article.import # => 0 ``` It's possible to import only records from a specific `scope` or `query`, transform the batch with the `transform` and `preprocess` options, or re-create the index by deleting it and creating it with correct mapping with the `force` option -- look for examples in the method documentation. No errors were reported during importing, so... let's search the index! ### Searching For starters, we can try the "simple" type of search: ```ruby response = Article.search 'fox dogs' response.took # => 3 response.results.total # => 2 response.results.first._score # => 0.02250402 response.results.first._source.title # => "Quick brown fox" ``` #### Search results The returned `response` object is a rich wrapper around the JSON returned from Elasticsearch, providing access to response metadata and the actual results ("hits"). Each "hit" is wrapped in the `Result` class, and provides method access to its properties via [`Hashie::Mash`](http://github.com/intridea/hashie). The `results` object supports the `Enumerable` interface: ```ruby response.results.map { |r| r._source.title } # => ["Quick brown fox", "Fast black dogs"] response.results.select { |r| r.title =~ /^Q/ } # => [#{"title"=>"Quick brown fox"}}>] ``` In fact, the `response` object will delegate `Enumerable` methods to `results`: ```ruby response.any? { |r| r.title =~ /fox|dog/ } # => true ``` To use `Array`'s methods (including any _ActiveSupport_ extensions), just call `to_a` on the object: ```ruby response.to_a.last.title # "Fast black dogs" ``` #### Search results as database records Instead of returning documents from Elasticsearch, the `records` method will return a collection of model instances, fetched from the primary database, ordered by score: ```ruby response.records.to_a # Article Load (0.3ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 2) # => [#
, #
] ``` The returned object is the genuine collection of model instances returned by your database, i.e. `ActiveRecord::Relation` for ActiveRecord, or `Mongoid::Criteria` in case of MongoDB. This allows you to chain other methods on top of search results, as you would normally do: ```ruby response.records.where(title: 'Quick brown fox').to_a # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 2) AND "articles"."title" = 'Quick brown fox' # => [#
] response.records.records.class # => ActiveRecord::Relation::ActiveRecord_Relation_Article ``` The ordering of the records by score will be preserved, unless you explicitly specify a different order in your model query language: ```ruby response.records.order(:title).to_a # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 2) ORDER BY "articles".title ASC # => [#
, #
] ``` The `records` method returns the real instances of your model, which is useful when you want to access your model methods -- at the expense of slowing down your application, of course. In most cases, working with `results` coming from Elasticsearch is sufficient, and much faster. See the [`elasticsearch-rails`](https://github.com/elastic/elasticsearch-rails/tree/main/elasticsearch-rails) library for more information about compatibility with the Ruby on Rails framework. When you want to access both the database `records` and search `results`, use the `each_with_hit` (or `map_with_hit`) iterator: ```ruby response.records.each_with_hit { |record, hit| puts "* #{record.title}: #{hit._score}" } # * Quick brown fox: 0.02250402 # * Fast black dogs: 0.02250402 ``` #### Searching multiple models It is possible to search across multiple models with the module method: ```ruby Elasticsearch::Model.search('fox', [Article, Comment]).results.to_a.map(&:to_hash) # => [ # {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_score"=>0.35136628, "_source"=>...}, # {"_index"=>"comments", "_type"=>"comment", "_id"=>"1", "_score"=>0.35136628, "_source"=>...} # ] Elasticsearch::Model.search('fox', [Article, Comment]).records.to_a # Article Load (0.3ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1) # Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."id" IN (1,5) # => [#
, #, ...] ``` By default, all models which include the `Elasticsearch::Model` module are searched. NOTE: It is _not_ possible to chain other methods on top of the `records` object, since it is a heterogenous collection, with models potentially backed by different databases. #### Pagination You can implement pagination with the `from` and `size` search parameters. However, search results can be automatically paginated with the [`kaminari`](http://rubygems.org/gems/kaminari) or [`will_paginate`](https://github.com/mislav/will_paginate) gems. (The pagination gems must be added before the Elasticsearch gems in your Gemfile, or loaded first in your application.) If Kaminari or WillPaginate is loaded, use the familiar paging methods: ```ruby response.page(2).results response.page(2).records ``` In a Rails controller, use the `params[:page]` parameter to paginate through results: ```ruby @articles = Article.search(params[:q]).page(params[:page]).records @articles.current_page # => 2 @articles.next_page # => 3 ``` To initialize and include the Kaminari pagination support manually: ```ruby Kaminari::Hooks.init if defined?(Kaminari::Hooks) Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari ``` #### The Elasticsearch DSL In most situations, you'll want to pass the search definition in the Elasticsearch [domain-specific language](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) to the client: ```ruby response = Article.search query: { match: { title: "Fox Dogs" } }, highlight: { fields: { title: {} } } response.results.first.highlight.title # ["Quick brown fox"] ``` You can pass any object which implements a `to_hash` method, which is called automatically, so you can use a custom class or your favourite JSON builder to build the search definition: ```ruby require 'jbuilder' query = Jbuilder.encode do |json| json.query do json.match do json.title do json.query "fox dogs" end end end end response = Article.search query response.results.first.title # => "Quick brown fox" ``` Also, you can use the [**`elasticsearch-dsl`**](https://github.com/elastic/elasticsearch-ruby/tree/main/elasticsearch-dsl) library, which provides a specialized Ruby API for the Elasticsearch Query DSL: ```ruby require 'elasticsearch/dsl' query = Elasticsearch::DSL::Search.search do query do match :title do query 'fox dogs' end end end response = Article.search query response.results.first.title # => "Quick brown fox" ``` ### Index Configuration For proper search engine function, it's often necessary to configure the index properly. The `Elasticsearch::Model` integration provides class methods to set up index settings and mappings. **NOTE**: Elasticsearch will automatically create an index when a document is indexed, with default settings and mappings. Create the index in advance with the `create_index!` method, so your index configuration is respected. ```ruby class Article settings index: { number_of_shards: 1 } do mappings dynamic: 'false' do indexes :title, analyzer: 'english', index_options: 'offsets' end end end Article.mappings.to_hash # => { # :article => { # :dynamic => "false", # :properties => { # :title => { # :type => "string", # :analyzer => "english", # :index_options => "offsets" # } # } # } # } Article.settings.to_hash # { :index => { :number_of_shards => 1 } } ``` You can use the defined settings and mappings to create an index with desired configuration: ```ruby Article.__elasticsearch__.client.indices.delete index: Article.index_name rescue nil Article.__elasticsearch__.client.indices.create \ index: Article.index_name, body: { settings: Article.settings.to_hash, mappings: Article.mappings.to_hash } ``` There's a shortcut available for this common operation (convenient e.g. in tests): ```ruby Article.__elasticsearch__.create_index! force: true Article.__elasticsearch__.refresh_index! ``` By default, index name and document type will be inferred from your class name, you can set it explicitly, however: ```ruby class Article index_name "articles-#{Rails.env}" document_type "post" end ``` ### Updating the Documents in the Index Usually, we need to update the Elasticsearch index when records in the database are created, updated or deleted; use the `index_document`, `update_document` and `delete_document` methods, respectively: ```ruby Article.first.__elasticsearch__.index_document # => {"ok"=>true, ... "_version"=>2} ``` #### Automatic Callbacks You can automatically update the index whenever the record changes, by including the `Elasticsearch::Model::Callbacks` module in your model: ```ruby class Article include Elasticsearch::Model include Elasticsearch::Model::Callbacks end Article.first.update_attribute :title, 'Updated!' Article.search('*').map { |r| r.title } # => ["Updated!", "Lime green frogs", "Fast black dogs"] ``` The automatic callback on record update keeps track of changes in your model (via [`ActiveModel::Dirty`](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)-compliant implementation), and performs a _partial update_ when this support is available. The automatic callbacks are implemented in database adapters coming with `Elasticsearch::Model`. You can easily implement your own adapter: please see the relevant chapter below. #### Custom Callbacks In case you would need more control of the indexing process, you can implement these callbacks yourself, by hooking into `after_create`, `after_save`, `after_update` or `after_destroy` operations: ```ruby class Article include Elasticsearch::Model after_save { logger.debug ["Updating document... ", index_document ].join } after_destroy { logger.debug ["Deleting document... ", delete_document].join } end ``` For ActiveRecord-based models, use the `after_commit` callback to protect your data against inconsistencies caused by transaction rollbacks: ```ruby class Article < ActiveRecord::Base include Elasticsearch::Model after_commit on: [:create] do __elasticsearch__.index_document if self.published? end after_commit on: [:update] do if self.published? __elasticsearch__.update_document else __elasticsearch__.delete_document end end after_commit on: [:destroy] do __elasticsearch__.delete_document if self.published? end end ``` #### Asynchronous Callbacks Of course, you're still performing an HTTP request during your database transaction, which is not optimal for large-scale applications. A better option would be to process the index operations in background, with a tool like [_Resque_](https://github.com/resque/resque) or [_Sidekiq_](https://github.com/mperham/sidekiq): ```ruby class Article include Elasticsearch::Model after_save { Indexer.perform_async(:index, self.id) } after_destroy { Indexer.perform_async(:delete, self.id) } end ``` An example implementation of the `Indexer` worker class could look like this: ```ruby class Indexer include Sidekiq::Worker sidekiq_options queue: 'elasticsearch', retry: false Logger = Sidekiq.logger.level == Logger::DEBUG ? Sidekiq.logger : nil Client = Elasticsearch::Client.new host: 'localhost:9200', logger: Logger def perform(operation, record_id) logger.debug [operation, "ID: #{record_id}"] case operation.to_s when /index/ record = Article.find(record_id) Client.index index: 'articles', type: 'article', id: record.id, body: record.__elasticsearch__.as_indexed_json when /delete/ begin Client.delete index: 'articles', type: 'article', id: record_id rescue Elasticsearch::Transport::Transport::Errors::NotFound logger.debug "Article not found, ID: #{record_id}" end else raise ArgumentError, "Unknown operation '#{operation}'" end end end ``` Start the _Sidekiq_ workers with `bundle exec sidekiq --queue elasticsearch --verbose` and update a model: ```ruby Article.first.update_attribute :title, 'Updated' ``` You'll see the job being processed in the console where you started the _Sidekiq_ worker: ``` Indexer JID-eb7e2daf389a1e5e83697128 DEBUG: ["index", "ID: 7"] Indexer JID-eb7e2daf389a1e5e83697128 INFO: PUT http://localhost:9200/articles/article/1 [status:200, request:0.004s, query:n/a] Indexer JID-eb7e2daf389a1e5e83697128 DEBUG: > {"id":1,"title":"Updated", ...} Indexer JID-eb7e2daf389a1e5e83697128 DEBUG: < {"ok":true,"_index":"articles","_type":"article","_id":"1","_version":6} Indexer JID-eb7e2daf389a1e5e83697128 INFO: done: 0.006 sec ``` ### Model Serialization By default, the model instance will be serialized to JSON using the `as_indexed_json` method, which is defined automatically by the `Elasticsearch::Model::Serializing` module: ```ruby Article.first.__elasticsearch__.as_indexed_json # => {"id"=>1, "title"=>"Quick brown fox"} ``` If you want to customize the serialization, just implement the `as_indexed_json` method yourself, for instance with the [`as_json`](http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json) method: ```ruby class Article include Elasticsearch::Model def as_indexed_json(options={}) as_json(only: 'title') end end Article.first.as_indexed_json # => {"title"=>"Quick brown fox"} ``` The re-defined method will be used in the indexing methods, such as `index_document`. Please note that in Rails 3, you need to either set `include_root_in_json: false`, or prevent adding the "root" in the JSON representation with other means. #### Relationships and Associations When you have a more complicated structure/schema, you need to customize the `as_indexed_json` method - or perform the indexing separately, on your own. For example, let's have an `Article` model, which _has_many_ `Comment`s, `Author`s and `Categories`. We might want to define the serialization like this: ```ruby def as_indexed_json(options={}) self.as_json( include: { categories: { only: :title}, authors: { methods: [:full_name], only: [:full_name] }, comments: { only: :text } }) end Article.first.as_indexed_json # => { "id" => 1, # "title" => "First Article", # "created_at" => 2013-12-03 13:39:02 UTC, # "updated_at" => 2013-12-03 13:39:02 UTC, # "categories" => [ { "title" => "One" } ], # "authors" => [ { "full_name" => "John Smith" } ], # "comments" => [ { "text" => "First comment" } ] } ``` Of course, when you want to use the automatic indexing callbacks, you need to hook into the appropriate _ActiveRecord_ callbacks -- please see the full example in `examples/activerecord_associations.rb`. ### Other ActiveModel Frameworks The `Elasticsearch::Model` module is fully compatible with any ActiveModel-compatible model, such as _Mongoid_: ```ruby require 'mongoid' Mongoid.connect_to 'articles' class Article include Mongoid::Document field :id, type: String field :title, type: String attr_accessible :id, :title, :published_at include Elasticsearch::Model def as_indexed_json(options={}) as_json(except: [:id, :_id]) end end Article.create id: '1', title: 'Quick brown fox' Article.import response = Article.search 'fox'; response.records.to_a # MOPED: 127.0.0.1:27017 QUERY database=articles collection=articles selector={"_id"=>{"$in"=>["1"]}} ... # => [#
] ``` Full examples for CouchBase, DataMapper, Mongoid, Ohm and Riak models can be found in the `examples` folder. ### Adapters To support various "OxM" (object-relational- or object-document-mapper) implementations and frameworks, the `Elasticsearch::Model` integration supports an "adapter" concept. An adapter provides implementations for common behaviour, such as fetching records from the database, hooking into model callbacks for automatic index updates, or efficient bulk loading from the database. The integration comes with adapters for _ActiveRecord_ and _Mongoid_ out of the box. Writing an adapter for your favourite framework is straightforward -- let's see a simplified adapter for [_DataMapper_](http://datamapper.org): ```ruby module DataMapperAdapter # Implement the interface for fetching records # module Records def records klass.all(id: ids) end # ... end end # Register the adapter # Elasticsearch::Model::Adapter.register( DataMapperAdapter, lambda { |klass| defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource) } ) ``` Require the adapter and include `Elasticsearch::Model` in the class: ```ruby require 'datamapper_adapter' class Article include DataMapper::Resource include Elasticsearch::Model property :id, Serial property :title, String end ``` When accessing the `records` method of the response, for example, the implementation from our adapter will be used now: ```ruby response = Article.search 'foo' response.records.to_a # ~ (0.000057) SELECT "id", "title", "published_at" FROM "articles" WHERE "id" IN (3, 1) ORDER BY "id" # => [#
, #
] response.records.records.class # => DataMapper::Collection ``` More examples can be found in the `examples` folder. Please see the `Elasticsearch::Model::Adapter` module and its submodules for technical information. ### Settings The module provides a common `settings` method to customize various features. Before version 7.0.0 of the gem, the only supported setting was `:inheritance_enabled`. This setting has been deprecated and removed. ## Development and Community For local development, clone the repository and run `bundle install`. See `rake -T` for a list of available Rake tasks for running tests, generating documentation, starting a testing cluster, etc. Bug fixes and features must be covered by unit tests. Github's pull requests and issues are used to communicate, send bug reports and code contributions. To run all tests against a test Elasticsearch cluster, use a command like this: ```bash curl -# https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.RC1.tar.gz | tar xz -C tmp/ SERVER=start TEST_CLUSTER_COMMAND=$PWD/tmp/elasticsearch-1.0.0.RC1/bin/elasticsearch bundle exec rake test:all ``` ### Single Table Inheritance support Versions < 7.0.0 of this gem supported inheritance-- more specifically, `Single Table Inheritance`. With this feature, elasticsearch settings (index mappings, etc) on a parent model could be inherited by a child model leading to different model documents being indexed into the same Elasticsearch index. This feature depended on the ability to set a `type` for a document in Elasticsearch. The Elasticsearch team has deprecated support for `types`, as is described [here.](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html) This gem will also remove support for types and `Single Table Inheritance` in version 7.0 as it enables an anti-pattern. Please save different model documents in separate indices. If you want to use STI, you can include an artificial `type` field manually in each document and use it in other operations. ## License This software is licensed under the Apache 2 license, quoted below. Licensed to Elasticsearch B.V. under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. Elasticsearch B.V. licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. elasticsearch-model-7.2.1/gemfiles/0000755000004100000410000000000014217570270017261 5ustar www-datawww-dataelasticsearch-model-7.2.1/gemfiles/4.0.gemfile0000644000004100000410000000226414217570270021120 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Usage: # # $ BUNDLE_GEMFILE=./gemfiles/4.0.gemfile bundle install # $ BUNDLE_GEMFILE=./gemfiles/4.0.gemfile bundle exec rake test:integration source 'https://rubygems.org' gemspec path: '../' gem 'activemodel', '~> 4' gem 'activerecord', '~> 4' gem 'mongoid', '~> 5' gem 'sqlite3', '> 1.3', '< 1.4' unless defined?(JRUBY_VERSION) group :development, :testing do gem 'bigdecimal', '~> 1' gem 'pry-nav' gem 'rspec' end elasticsearch-model-7.2.1/gemfiles/3.0.gemfile0000644000004100000410000000224314217570270021114 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Usage: # # $ BUNDLE_GEMFILE=./gemfiles/3.0.gemfile bundle install # $ BUNDLE_GEMFILE=./gemfiles/3.0.gemfile bundle exec rake test:integration source 'https://rubygems.org' gemspec path: '../' gem 'activemodel', '>= 3.0' gem 'activerecord', '~> 3.2' gem 'mongoid', '>= 3.0' gem 'sqlite3', '> 1.3', '< 1.4' unless defined?(JRUBY_VERSION) group :development, :testing do gem 'rspec' gem 'pry-nav' endelasticsearch-model-7.2.1/gemfiles/5.0.gemfile0000644000004100000410000000220714217570270021116 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Usage: # # $ BUNDLE_GEMFILE=./gemfiles/5.0.gemfile bundle install # $ BUNDLE_GEMFILE=./gemfiles/5.0.gemfile bundle exec rake test:integration source 'https://rubygems.org' gemspec path: '../' gem 'activemodel', '~> 5' gem 'activerecord', '~> 5' gem 'sqlite3' unless defined?(JRUBY_VERSION) gem 'mongoid', '~> 6' group :development, :testing do gem 'rspec' gem 'pry-nav' end elasticsearch-model-7.2.1/gemfiles/6.0.gemfile0000644000004100000410000000221314217570270021114 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Usage: # # $ BUNDLE_GEMFILE=./gemfiles/6.0.gemfile bundle install # $ BUNDLE_GEMFILE=./gemfiles/6.0.gemfile bundle exec rake test:integration source 'https://rubygems.org' gemspec path: '../' gem 'activemodel', '6.0.0' gem 'activerecord', '6.0.0' gem 'sqlite3' unless defined?(JRUBY_VERSION) #gem 'mongoid', '~> 6' group :development, :testing do gem 'rspec' gem 'pry-nav' end elasticsearch-model-7.2.1/spec/0000755000004100000410000000000014217570270016420 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/spec_helper.rb0000644000004100000410000001240314217570270021236 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'pry-nav' require 'kaminari' require 'kaminari/version' require 'will_paginate' require 'will_paginate/collection' require 'elasticsearch/model' require 'hashie/version' require 'active_model' begin require 'mongoid' rescue LoadError $stderr.puts("'mongoid' gem could not be loaded") end require 'yaml' require 'active_record' unless defined?(ELASTICSEARCH_URL) ELASTICSEARCH_URL = ENV['ELASTICSEARCH_URL'] || "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9200)}" end RSpec.configure do |config| config.formatter = 'documentation' config.color = true config.before(:suite) do require 'ansi' tracer = ::Logger.new(STDERR) tracer.formatter = lambda { |s, d, p, m| "#{m.gsub(/^.*$/) { |n| ' ' + n }.ansi(:faint)}\n" } Elasticsearch::Model.client = Elasticsearch::Client.new host: ELASTICSEARCH_URL, tracer: (ENV['QUIET'] ? nil : tracer) puts "Elasticsearch Version: #{Elasticsearch::Model.client.info['version']}" unless ActiveRecord::Base.connected? ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) end require 'support/app' if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' ::ActiveRecord::Base.raise_in_transactional_callbacks = true end end config.after(:all) do drop_all_tables! delete_all_indices! end end # Is the ActiveRecord version at least 4.0? # # @return [ true, false ] Whether the ActiveRecord version is at least 4.0. # # @since 6.0.1 def active_record_at_least_4? defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 end # Delete all documents from the indices of the provided list of models. # # @param [ Array ] models The list of models. # # @return [ true ] # # @since 6.0.1 def clear_indices(*models) models.each do |model| begin Elasticsearch::Model.client.delete_by_query( index: model.index_name, q: '*', body: {} ) rescue end end true end # Delete all documents from the tables of the provided list of models. # # @param [ Array ] models The list of models. # # @return [ true ] # # @since 6.0.1 def clear_tables(*models) begin; models.map(&:delete_all); rescue; end and true end # Drop all tables of models registered as subclasses of ActiveRecord::Base. # # @return [ true ] # # @since 6.0.1 def drop_all_tables! ActiveRecord::Base.descendants.each do |model| begin ActiveRecord::Schema.define do drop_table model end if model.table_exists? rescue end end and true end # Drop all indices of models registered as subclasses of ActiveRecord::Base. # # @return [ true ] # # @since 6.0.1 def delete_all_indices! client = Elasticsearch::Model.client ActiveRecord::Base.descendants.each do |model| begin client.indices.delete(index: model.index_name) if model.__elasticsearch__.index_exists? rescue end end and true end # Remove all classes. # # @param [ Array ] classes The list of classes to remove. # # @return [ true ] # # @since 6.0.1 def remove_classes(*classes) classes.each do |_class| Object.send(:remove_const, _class.name.to_sym) if defined?(_class) end and true end # Determine whether the tests with Mongoid should be run. # Depends on whether MongoDB is running on the default host and port, `localhost:27017`. # # @return [ true, false ] # # @since 6.0.1 def test_mongoid? $mongoid_available ||= begin require 'mongoid' if defined?(Mongo) # older versions of Mongoid use the driver, Moped client = Mongo::Client.new(['localhost:27017']) Timeout.timeout(1) do client.database.command(ping: 1) && true end end and true rescue LoadError $stderr.puts("'mongoid' gem could not be loaded") rescue Timeout::Error, Mongo::Error => e client.close if client $stderr.puts("MongoDB not installed or running: #{e}") end end # Connect Mongoid and set up its Logger if Mongoid tests should be run. # # @since 6.0.1 def connect_mongoid(source) if test_mongoid? $stderr.puts "Mongoid #{Mongoid::VERSION}", '-'*80 if !ENV['QUIET'] == 'true' logger = ::Logger.new($stderr) logger.formatter = lambda { |s, d, p, m| " #{m.ansi(:faint, :cyan)}\n" } logger.level = ::Logger::DEBUG Mongoid.logger = logger Mongo::Logger.logger = logger else Mongo::Logger.logger.level = ::Logger::WARN end Mongoid.connect_to(source) end end elasticsearch-model-7.2.1/spec/support/0000755000004100000410000000000014217570270020134 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/support/model.yml0000644000004100000410000000001514217570270021753 0ustar www-datawww-databaz: 'qux' elasticsearch-model-7.2.1/spec/support/app.rb0000644000004100000410000000314514217570270021244 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'active_record' require 'support/app/question' require 'support/app/answer' require 'support/app/parent_and_child_searchable' require 'support/app/article_with_custom_serialization' require 'support/app/import_article' require 'support/app/namespaced_book' require 'support/app/article_for_pagination' require 'support/app/article_with_dynamic_index_name' require 'support/app/episode' require 'support/app/series' require 'support/app/article' require 'support/app/article_no_type' require 'support/app/searchable' require 'support/app/category' require 'support/app/author' require 'support/app/authorship' require 'support/app/comment' require 'support/app/post' # Mongoid models begin require 'support/app/image' require 'support/app/mongoid_article' rescue $stderr.puts("'mongoid' gem is not installed, could not load Mongoid models") end elasticsearch-model-7.2.1/spec/support/app/0000755000004100000410000000000014217570270020714 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/support/app/answer.rb0000644000004100000410000000336514217570270022547 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Answer < ActiveRecord::Base include Elasticsearch::Model belongs_to :question JOIN_TYPE = 'answer'.freeze index_name 'questions_and_answers'.freeze document_type 'doc'.freeze before_create :randomize_id def randomize_id begin self.id = SecureRandom.random_number(1_000_000) end while Answer.where(id: self.id).exists? end mapping do indexes :text indexes :author end def as_indexed_json(options={}) # This line is necessary for differences between ActiveModel::Serializers::JSON#as_json versions json = as_json(options)[JOIN_TYPE] || as_json(options) json.merge(join_field: { name: JOIN_TYPE, parent: question_id }) end after_commit lambda { __elasticsearch__.index_document(routing: (question_id || 1)) }, on: :create after_commit lambda { __elasticsearch__.update_document(routing: (question_id || 1)) }, on: :update after_commit lambda {__elasticsearch__.delete_document(routing: (question_id || 1)) }, on: :destroy end elasticsearch-model-7.2.1/spec/support/app/comment.rb0000644000004100000410000000151714217570270022707 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Comment < ActiveRecord::Base belongs_to :post, touch: true end elasticsearch-model-7.2.1/spec/support/app/mongoid_article.rb0000644000004100000410000000245114217570270024402 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ::MongoidArticle include Mongoid::Document include Elasticsearch::Model include Elasticsearch::Model::Callbacks field :id, type: String field :title, type: String field :views, type: Integer attr_accessible :title if respond_to? :attr_accessible settings index: { number_of_shards: 1, number_of_replicas: 0 } do mapping do indexes :title, type: 'text', analyzer: 'snowball' indexes :created_at, type: 'date' end end def as_indexed_json(options={}) as_json(except: [:id, :_id]) end end elasticsearch-model-7.2.1/spec/support/app/image.rb0000644000004100000410000000232714217570270022327 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Image include Mongoid::Document include Elasticsearch::Model include Elasticsearch::Model::Callbacks field :name, type: String attr_accessible :name if respond_to? :attr_accessible settings index: {number_of_shards: 1, number_of_replicas: 0} do mapping do indexes :name, type: 'text', analyzer: 'snowball' indexes :created_at, type: 'date' end end def as_indexed_json(options={}) as_json(except: [:_id]) end end elasticsearch-model-7.2.1/spec/support/app/authorship.rb0000644000004100000410000000154714217570270023436 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Authorship < ActiveRecord::Base belongs_to :author belongs_to :post, touch: true end elasticsearch-model-7.2.1/spec/support/app/author.rb0000644000004100000410000000167614217570270022555 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Author < ActiveRecord::Base has_many :authorships after_update { self.authorships.each(&:touch) } def full_name [first_name, last_name].compact.join(' ') end end elasticsearch-model-7.2.1/spec/support/app/episode.rb0000644000004100000410000000207014217570270022670 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Episode < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks settings index: {number_of_shards: 1, number_of_replicas: 0} do mapping do indexes :name, type: 'text', analyzer: 'snowball' indexes :created_at, type: 'date' end end end elasticsearch-model-7.2.1/spec/support/app/import_article.rb0000644000004100000410000000206414217570270024260 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ImportArticle < ActiveRecord::Base include Elasticsearch::Model scope :popular, -> { where('views >= 5') } mapping do indexes :title, type: 'text' indexes :views, type: 'integer' indexes :numeric, type: 'integer' indexes :created_at, type: 'date' end end elasticsearch-model-7.2.1/spec/support/app/article_with_custom_serialization.rb0000644000004100000410000000206314217570270030247 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ::ArticleWithCustomSerialization < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks mapping do indexes :title end def as_indexed_json(options={}) # as_json(options.merge root: false).slice('title') { title: self.title } end end elasticsearch-model-7.2.1/spec/support/app/namespaced_book.rb0000644000004100000410000000171614217570270024360 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module MyNamespace class Book < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks document_type 'book' mapping { indexes :title } end end elasticsearch-model-7.2.1/spec/support/app/parent_and_child_searchable.rb0000644000004100000410000000325514217570270026675 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module ParentChildSearchable INDEX_NAME = 'questions_and_answers'.freeze JOIN = 'join'.freeze def create_index!(options={}) client = Question.__elasticsearch__.client client.indices.delete index: INDEX_NAME rescue nil if options.delete(:force) settings = Question.settings.to_hash.merge Answer.settings.to_hash mapping_properties = { join_field: { type: JOIN, relations: { Question::JOIN_TYPE => Answer::JOIN_TYPE } } } merged_properties = mapping_properties.merge(Question.mappings.to_hash[:doc][:properties]).merge( Answer.mappings.to_hash[:doc][:properties]) mappings = { doc: { properties: merged_properties }} client.indices.create({ index: INDEX_NAME, body: { settings: settings.to_hash, mappings: mappings } }.merge(options)) end extend self end elasticsearch-model-7.2.1/spec/support/app/article_no_type.rb0000644000004100000410000000246514217570270024430 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ::ArticleNoType < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks settings index: {number_of_shards: 1, number_of_replicas: 0} do mapping do indexes :title, type: 'text', analyzer: 'snowball' indexes :body, type: 'text' indexes :clicks, type: 'integer' indexes :created_at, type: 'date' end end def as_indexed_json(options = {}) attributes .symbolize_keys .slice(:title, :body, :clicks, :created_at) .merge(suggest_title: title) end end elasticsearch-model-7.2.1/spec/support/app/article_for_pagination.rb0000644000004100000410000000213014217570270025737 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ::ArticleForPagination < ActiveRecord::Base include Elasticsearch::Model scope :published, -> { where(published: true) } settings index: { number_of_shards: 1, number_of_replicas: 0 } do mapping do indexes :title, type: 'text', analyzer: 'snowball' indexes :created_at, type: 'date' end end end elasticsearch-model-7.2.1/spec/support/app/article.rb0000644000004100000410000000251214217570270022664 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ::Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks document_type 'article' settings index: {number_of_shards: 1, number_of_replicas: 0} do mapping do indexes :title, type: 'text', analyzer: 'snowball' indexes :body, type: 'text' indexes :clicks, type: 'integer' indexes :created_at, type: 'date' end end def as_indexed_json(options = {}) attributes .symbolize_keys .slice(:title, :body, :clicks, :created_at) .merge(suggest_title: title) end end elasticsearch-model-7.2.1/spec/support/app/series.rb0000644000004100000410000000206714217570270022540 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Series < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks settings index: {number_of_shards: 1, number_of_replicas: 0} do mapping do indexes :name, type: 'text', analyzer: 'snowball' indexes :created_at, type: 'date' end end end elasticsearch-model-7.2.1/spec/support/app/searchable.rb0000644000004100000410000000373414217570270023341 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Searchable extend ActiveSupport::Concern included do include Elasticsearch::Model include Elasticsearch::Model::Callbacks # Set up the mapping # settings index: { number_of_shards: 1, number_of_replicas: 0 } do mapping do indexes :title, analyzer: 'snowball' indexes :created_at, type: 'date' indexes :authors do indexes :first_name indexes :last_name indexes :full_name, type: 'text' do indexes :raw, type: 'keyword' end end indexes :categories, type: 'keyword' indexes :comments, type: 'nested' do indexes :text indexes :author end end end # Customize the JSON serialization for Elasticsearch # def as_indexed_json(options={}) { title: title, text: text, categories: categories.map(&:title), authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]), comments: comments.as_json(only: [:text, :author]) } end # Update document in the index after touch # after_touch() { __elasticsearch__.index_document } end end elasticsearch-model-7.2.1/spec/support/app/article_with_dynamic_index_name.rb0000644000004100000410000000211114217570270027605 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class ::ArticleWithDynamicIndexName < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks def self.counter=(value) @counter = 0 end def self.counter (@counter ||= 0) && @counter += 1 end mapping { indexes :title } index_name { "articles-#{counter}" } end elasticsearch-model-7.2.1/spec/support/app/post.rb0000644000004100000410000000306214217570270022227 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Post < ActiveRecord::Base include Searchable has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] has_many :authorships has_many :authors, through: :authorships, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] has_many :comments, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] after_touch() { __elasticsearch__.index_document } end elasticsearch-model-7.2.1/spec/support/app/category.rb0000644000004100000410000000152114217570270023055 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Category < ActiveRecord::Base has_and_belongs_to_many :posts end elasticsearch-model-7.2.1/spec/support/app/question.rb0000644000004100000410000000305214217570270023110 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. class Question < ActiveRecord::Base include Elasticsearch::Model has_many :answers, dependent: :destroy JOIN_TYPE = 'question'.freeze JOIN_METADATA = { join_field: JOIN_TYPE}.freeze index_name 'questions_and_answers'.freeze document_type 'doc'.freeze mapping do indexes :title indexes :text indexes :author end def as_indexed_json(options={}) # This line is necessary for differences between ActiveModel::Serializers::JSON#as_json versions json = as_json(options)[JOIN_TYPE] || as_json(options) json.merge(JOIN_METADATA) end after_commit lambda { __elasticsearch__.index_document }, on: :create after_commit lambda { __elasticsearch__.update_document }, on: :update after_commit lambda { __elasticsearch__.delete_document }, on: :destroy end elasticsearch-model-7.2.1/spec/support/model.json0000644000004100000410000000002114217570270022120 0ustar www-datawww-data{ "laz": "qux" } elasticsearch-model-7.2.1/spec/elasticsearch/0000755000004100000410000000000014217570270021232 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/0000755000004100000410000000000014217570270022332 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/searching_search_request_spec.rb0000644000004100000410000000725414217570270030741 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Serializing do before(:all) do class ::DummySearchingModel extend Elasticsearch::Model::Searching::ClassMethods def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(DummySearchingModel) end before do allow(DummySearchingModel).to receive(:client).and_return(client) end let(:client) do double('client') end describe '#initialize' do context 'when the search definition is a simple query' do before do expect(client).to receive(:search).with(index: 'foo', type: 'bar', q: 'foo').and_return({}) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(DummySearchingModel, 'foo') end it 'passes the query to the client' do expect(search.execute!).to eq({}) end end context 'when the search definition is a hash' do before do expect(client).to receive(:search).with(index: 'foo', type: 'bar', body: { foo: 'bar' }).and_return({}) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(DummySearchingModel, foo: 'bar') end it 'passes the hash to the client' do expect(search.execute!).to eq({}) end end context 'when the search definition is a json string' do before do expect(client).to receive(:search).with(index: 'foo', type: 'bar', body: '{"foo":"bar"}').and_return({}) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(DummySearchingModel, '{"foo":"bar"}') end it 'passes the json string to the client' do expect(search.execute!).to eq({}) end end context 'when the search definition is a custom object' do before(:all) do class MySpecialQueryBuilder def to_hash; {foo: 'bar'}; end end end after(:all) do Object.send(:remove_const, :MySpecialQueryBuilder) if defined?(MySpecialQueryBuilder) end before do expect(client).to receive(:search).with(index: 'foo', type: 'bar', body: {foo: 'bar'}).and_return({}) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(DummySearchingModel, MySpecialQueryBuilder.new) end it 'passes the query builder to the client and calls #to_hash on it' do expect(search.execute!).to eq({}) end end context 'when extra options are specified' do before do expect(client).to receive(:search).with(index: 'foo', type: 'bar', q: 'foo', size: 15).and_return({}) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(DummySearchingModel, 'foo', size: 15) end it 'passes the extra options to the client as part of the request' do expect(search.execute!).to eq({}) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/0000755000004100000410000000000014217570270024135 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/multiple_spec.rb0000644000004100000410000000655414217570270027341 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter::Multiple do before(:all) do class DummyOne include Elasticsearch::Model index_name 'dummy' document_type 'dummy_one' def self.find(ids) ids.map { |id| new(id) } end attr_reader :id def initialize(id) @id = id.to_i end end module Namespace class DummyTwo include Elasticsearch::Model index_name 'dummy' document_type 'dummy_two' def self.find(ids) ids.map { |id| new(id) } end attr_reader :id def initialize(id) @id = id.to_i end end end class DummyTwo include Elasticsearch::Model index_name 'other_index' document_type 'dummy_two' def self.find(ids) ids.map { |id| new(id) } end attr_reader :id def initialize(id) @id = id.to_i end end end after(:all) do [DummyOne, Namespace::DummyTwo, DummyTwo].each do |adapter| Elasticsearch::Model::Adapter::Adapter.adapters.delete(adapter) end Namespace.send(:remove_const, :DummyTwo) if defined?(Namespace::DummyTwo) remove_classes(DummyOne, DummyTwo, Namespace) end let(:hits) do [ { _index: 'dummy', _type: 'dummy_two', _id: '2' }, { _index: 'dummy', _type: 'dummy_one', _id: '2' }, { _index: 'other_index', _type: 'dummy_two', _id: '1' }, { _index: 'dummy', _type: 'dummy_two', _id: '1' }, { _index: 'dummy', _type: 'dummy_one', _id: '3' } ] end let(:response) do double('response', response: { 'hits' => { 'hits' => hits } }) end let(:multimodel) do Elasticsearch::Model::Multimodel.new(DummyOne, DummyTwo, Namespace::DummyTwo) end describe '#records' do before do multimodel.class.send :include, Elasticsearch::Model::Adapter::Multiple::Records expect(multimodel).to receive(:response).at_least(:once).and_return(response) end it 'instantiates the correct types of instances' do expect(multimodel.records[0]).to be_a(Namespace::DummyTwo) expect(multimodel.records[1]).to be_a(DummyOne) expect(multimodel.records[2]).to be_a(DummyTwo) expect(multimodel.records[3]).to be_a(Namespace::DummyTwo) expect(multimodel.records[4]).to be_a(DummyOne) end it 'returns the results in the correct order' do expect(multimodel.records.map(&:id)).to eq([2, 2, 1, 1, 3]) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record_spec.rb0000644000004100000410000001472714217570270030320 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter::ActiveRecord do before(:all) do class DummyClassForActiveRecord; end end after(:all) do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyClassForActiveRecord) remove_classes(DummyClassForActiveRecord) end let(:model) do DummyClassForActiveRecord.new.tap do |m| allow(m).to receive(:response).and_return(double('response', response: response)) allow(m).to receive(:ids).and_return(ids) end end let(:response) do { 'hits' => {'hits' => [ {'_id' => 2 }, {'_id' => 1 } ]} } end let(:ids) do [2, 1] end let(:record_1) do double('record').tap do |rec| allow(rec).to receive(:id).and_return(1) end end let(:record_2) do double('record').tap do |rec| allow(rec).to receive(:id).and_return(2) end end let(:records) do [record_1, record_2].tap do |r| allow(r).to receive(:load).and_return(true) allow(r).to receive(:exec_queries).and_return(true) end end describe 'adapter registration' do before(:all) do DummyClassForActiveRecord.__send__ :include, Elasticsearch::Model::Adapter::ActiveRecord::Records end it 'can register an adapater' do expect(Elasticsearch::Model::Adapter.adapters[Elasticsearch::Model::Adapter::ActiveRecord]).not_to be_nil expect(Elasticsearch::Model::Adapter.adapters[Elasticsearch::Model::Adapter::ActiveRecord].call(DummyClassForActiveRecord)).to be(false) end end describe '#records' do before(:all) do DummyClassForActiveRecord.__send__ :include, Elasticsearch::Model::Adapter::ActiveRecord::Records end let(:instance) do model.tap do |inst| allow(inst).to receive(:klass).and_return(double('class', primary_key: :some_key, where: records)).at_least(:once) allow(inst).to receive(:order).and_return(double('class', primary_key: :some_key, where: records)).at_least(:once) end end it 'returns the list of records' do expect(instance.records).to eq(records) end it 'loads the records' do expect(instance.load).to eq(true) end context 'when :includes is specified' do before do expect(records).to receive(:includes).with([:submodel]).once.and_return(records) instance.options[:includes] = [:submodel] end it 'incorporates the includes option in the query' do expect(instance.records).to eq(records) end end end describe 'callbacks registration' do before do expect(DummyClassForActiveRecord).to receive(:after_commit).exactly(3).times end it 'should register the model class for callbacks' do Elasticsearch::Model::Adapter::ActiveRecord::Callbacks.included(DummyClassForActiveRecord) end end describe 'importing' do before do DummyClassForActiveRecord.__send__ :extend, Elasticsearch::Model::Adapter::ActiveRecord::Importing end context 'when an invalid scope is specified' do it 'raises a NoMethodError' do expect { DummyClassForActiveRecord.__find_in_batches(scope: :not_found_method) }.to raise_exception(NoMethodError) end end context 'when a valid scope is specified' do before do expect(DummyClassForActiveRecord).to receive(:find_in_batches).once.and_return([]) expect(DummyClassForActiveRecord).to receive(:published).once.and_return(DummyClassForActiveRecord) end it 'uses the scope' do expect(DummyClassForActiveRecord.__find_in_batches(scope: :published)).to eq([]) end end context 'allow query criteria to be specified' do before do expect(DummyClassForActiveRecord).to receive(:find_in_batches).once.and_return([]) expect(DummyClassForActiveRecord).to receive(:where).with(color: 'red').once.and_return(DummyClassForActiveRecord) end it 'uses the scope' do expect(DummyClassForActiveRecord.__find_in_batches(query: -> { where(color: 'red') })).to eq([]) end end context 'when preprocessing batches' do context 'if the query returns results' do before do class << DummyClassForActiveRecord def find_in_batches(options = {}, &block) yield [:a, :b] end def update_batch(batch) batch.collect { |b| b.to_s + '!' } end end end it 'applies the preprocessing method' do DummyClassForActiveRecord.__find_in_batches(preprocess: :update_batch) do |batch| expect(batch).to match(['a!', 'b!']) end end end context 'if the query does not return results' do before do class << DummyClassForActiveRecord def find_in_batches(options = {}, &block) yield [:a, :b] end def update_batch(batch) [] end end end it 'applies the preprocessing method' do DummyClassForActiveRecord.__find_in_batches(preprocess: :update_batch) do |batch| expect(batch).to match([]) end end end end context 'when transforming models' do let(:instance) do model.tap do |inst| allow(inst).to receive(:id).and_return(1) allow(inst).to receive(:__elasticsearch__).and_return(double('object', id: 1, as_indexed_json: {})) end end it 'returns an proc' do expect(DummyClassForActiveRecord.__transform.respond_to?(:call)).to be(true) end it 'provides a default transformation' do expect(DummyClassForActiveRecord.__transform.call(instance)).to eq(index: { _id: 1, data: {} }) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/default_spec.rb0000644000004100000410000000410214217570270027115 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter::Default do before(:all) do class DummyClassForDefaultAdapter; end DummyClassForDefaultAdapter.__send__ :include, Elasticsearch::Model::Adapter::Default::Records DummyClassForDefaultAdapter.__send__ :include, Elasticsearch::Model::Adapter::Default::Importing end after(:all) do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyClassForDefaultAdapter) remove_classes(DummyClassForDefaultAdapter) end let(:instance) do DummyClassForDefaultAdapter.new.tap do |m| allow(m).to receive(:klass).and_return(double('class', primary_key: :some_key, find: [1])).at_least(:once) end end it 'should have the default records implementation' do expect(instance.records).to eq([1]) end it 'should have the default Callback implementation' do expect(Elasticsearch::Model::Adapter::Default::Callbacks).to be_a(Module) end it 'should have the default Importing implementation' do expect { DummyClassForDefaultAdapter.new.__find_in_batches }.to raise_exception(Elasticsearch::Model::NotImplemented) end it 'should have the default transform implementation' do expect { DummyClassForDefaultAdapter.new.__transform }.to raise_exception(Elasticsearch::Model::NotImplemented) end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/mongoid_spec.rb0000644000004100000410000001571314217570270027137 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter::Mongoid do before(:all) do class DummyClassForMongoid; end ::Symbol.class_eval { def in; self; end } end after(:all) do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyClassForMongoid) remove_classes(DummyClassForMongoid) end let(:response) do { 'hits' => {'hits' => [ {'_id' => 2}, {'_id' => 1} ]} } end let(:ids) do [2, 1] end let(:record_1) do double('record').tap do |rec| allow(rec).to receive(:id).and_return(1) end end let(:record_2) do double('record').tap do |rec| allow(rec).to receive(:load).and_return(true) allow(rec).to receive(:id).and_return(2) end end let(:records) do [record_1, record_2] end let(:model) do DummyClassForMongoid.new.tap do |m| allow(m).to receive(:response).and_return(double('response', response: response)) allow(m).to receive(:ids).and_return(ids) end end describe 'adapter registration' do it 'registers an adapater' do expect(Elasticsearch::Model::Adapter.adapters[Elasticsearch::Model::Adapter::Mongoid]).not_to be_nil expect(Elasticsearch::Model::Adapter.adapters[Elasticsearch::Model::Adapter::Mongoid].call(DummyClassForMongoid)).to be(false) end it 'registers the records module' do expect(Elasticsearch::Model::Adapter::Mongoid::Records).to be_a(Module) end end describe '#records' do before(:all) do DummyClassForMongoid.__send__ :include, Elasticsearch::Model::Adapter::Mongoid::Records end let(:instance) do model.tap do |inst| allow(inst).to receive(:klass).and_return(double('class', where: records)).at_least(:once) end end it 'returns the records' do expect(instance.records).to eq(records) end context 'when an order is not defined for the Mongoid query' do context 'when the records have a different order than the hits' do before do records.instance_variable_set(:@records, records) end it 'reorders the records based on hits order' do expect(records.collect(&:id)).to eq([1, 2]) expect(instance.records.to_a.collect(&:id)).to eq([2, 1]) end end context 'when an order is defined for the Mongoid query' do context 'when the records have a different order than the hits' do before do records.instance_variable_set(:@records, records) expect(instance.records).to receive(:asc).and_return(records) end it 'reorders the records based on hits order' do expect(records.collect(&:id)).to eq([1, 2]) expect(instance.records.to_a.collect(&:id)).to eq([2, 1]) expect(instance.asc.to_a.collect(&:id)).to eq([1, 2]) end end end end describe 'callbacks registration' do before do expect(DummyClassForMongoid).to receive(:after_create).once expect(DummyClassForMongoid).to receive(:after_update).once expect(DummyClassForMongoid).to receive(:after_destroy).once end it 'should register the model class for callbacks' do Elasticsearch::Model::Adapter::Mongoid::Callbacks.included(DummyClassForMongoid) end end end describe 'importing' do before(:all) do DummyClassForMongoid.__send__ :extend, Elasticsearch::Model::Adapter::Mongoid::Importing end let(:relation) do double('relation', each_slice: []).tap do |rel| allow(rel).to receive(:published).and_return(rel) allow(rel).to receive(:no_timeout).and_return(rel) allow(rel).to receive(:class_exec).and_return(rel) end end before do allow(DummyClassForMongoid).to receive(:all).and_return(relation) end context 'when a scope is specified' do it 'applies the scope' do expect(DummyClassForMongoid.__find_in_batches(scope: :published) do; end).to eq([]) end end context 'query criteria specified as a proc' do let(:query) do Proc.new { where(color: "red") } end it 'execites the query' do expect(DummyClassForMongoid.__find_in_batches(query: query) do; end).to eq([]) end end context 'query criteria specified as a hash' do before do expect(relation).to receive(:where).with(color: 'red').and_return(relation) end let(:query) do { color: "red" } end it 'execites the query' do expect(DummyClassForMongoid.__find_in_batches(query: query) do; end).to eq([]) end end context 'when preprocessing batches' do context 'if the query returns results' do before do class << DummyClassForMongoid def find_in_batches(options = {}, &block) yield [:a, :b] end def update_batch(batch) batch.collect { |b| b.to_s + '!' } end end end it 'applies the preprocessing method' do DummyClassForMongoid.__find_in_batches(preprocess: :update_batch) do |batch| expect(batch).to match(['a!', 'b!']) end end end context 'if the query does not return results' do before do class << DummyClassForMongoid def find_in_batches(options = {}, &block) yield [:a, :b] end def update_batch(batch) [] end end end it 'applies the preprocessing method' do DummyClassForMongoid.__find_in_batches(preprocess: :update_batch) do |batch| expect(batch).to match([]) end end end end context 'when transforming models' do let(:instance) do model.tap do |inst| allow(inst).to receive(:as_indexed_json).and_return({}) allow(inst).to receive(:id).and_return(1) end end it 'returns an proc' do expect(DummyClassForMongoid.__transform.respond_to?(:call)).to be(true) end it 'provides a default transformation' do expect(DummyClassForMongoid.__transform.call(instance)).to eq(index: { _id: '1', data: {} }) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/mongoid/0000755000004100000410000000000014217570270025571 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/mongoid/multi_model_spec.rb0000644000004100000410000000552614217570270031452 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Multimodel', if: test_mongoid? do before(:all) do connect_mongoid('mongoid_test') begin ActiveRecord::Schema.define(:version => 1) do create_table Episode.table_name do |t| t.string :name t.datetime :created_at, :default => 'NOW()' end end rescue end end before do clear_tables(Episode, Image) Episode.__elasticsearch__.create_index! force: true Episode.create name: 'TheEpisode' Episode.create name: 'A great Episode' Episode.create name: 'The greatest Episode' Episode.__elasticsearch__.refresh_index! Image.__elasticsearch__.create_index! force: true Image.create! name: 'The Image' Image.create! name: 'A great Image' Image.create! name: 'The greatest Image' Image.__elasticsearch__.refresh_index! Image.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' end after do [Episode, Image].each do |model| model.__elasticsearch__.client.delete_by_query(index: model.index_name, q: '*', body: {}) model.delete_all model.__elasticsearch__.refresh_index! end end context 'when the search is across multimodels with different adapters' do let(:search_result) do Elasticsearch::Model.search(%q<"greatest Episode" OR "greatest Image"^2>, [Episode, Image]) end it 'executes the search across models' do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end it 'returns the correct type of model instance' do expect(search_result.records[0]).to be_a(Image) expect(search_result.records[1]).to be_a(Episode) end it 'creates the model instances with the correct attributes' do expect(search_result.results[0].name).to eq('The greatest Image') expect(search_result.records[0].name).to eq('The greatest Image') expect(search_result.results[1].name).to eq('The greatest Episode') expect(search_result.records[1].name).to eq('The greatest Episode') end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/mongoid/basic_spec.rb0000644000004100000410000002021014217570270030204 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter::Mongoid, if: test_mongoid? do before(:all) do connect_mongoid('mongoid_test') Elasticsearch::Model::Adapter.register \ Elasticsearch::Model::Adapter::Mongoid, lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) } MongoidArticle.__elasticsearch__.create_index! force: true MongoidArticle.delete_all MongoidArticle.__elasticsearch__.refresh_index! MongoidArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' end after do clear_indices(MongoidArticle) clear_tables(MongoidArticle) end describe 'searching' do before do MongoidArticle.create! title: 'Test' MongoidArticle.create! title: 'Testing Coding' MongoidArticle.create! title: 'Coding' MongoidArticle.__elasticsearch__.refresh_index! end let(:search_result) do MongoidArticle.search('title:test') end it 'find the documents successfully' do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end describe '#results' do it 'returns a Elasticsearch::Model::Response::Result' do expect(search_result.results.first).to be_a(Elasticsearch::Model::Response::Result) end it 'retrieves the document from Elasticsearch' do expect(search_result.results.first.title).to eq('Test') end it 'retrieves all results' do expect(search_result.results.collect(&:title)).to match(['Test', 'Testing Coding']) end end describe '#records' do it 'returns an instance of the model' do expect(search_result.records.first).to be_a(MongoidArticle) end it 'retrieves the document from Elasticsearch' do expect(search_result.records.first.title).to eq('Test') end it 'iterates over the records' do expect(search_result.records.first.title).to eq('Test') end it 'retrieves all records' do expect(search_result.records.collect(&:title)).to match(['Test', 'Testing Coding']) end describe '#each_with_hit' do it 'yields each hit with the model object' do search_result.records.each_with_hit do |r, h| expect(h._source).not_to be_nil expect(h._source.title).not_to be_nil end end it 'preserves the search order' do search_result.records.each_with_hit do |r, h| expect(r.id.to_s).to eq(h._id) end end end describe '#map_with_hit' do it 'yields each hit with the model object' do search_result.records.map_with_hit do |r, h| expect(h._source).not_to be_nil expect(h._source.title).not_to be_nil end end it 'preserves the search order' do search_result.records.map_with_hit do |r, h| expect(r.id.to_s).to eq(h._id) end end end end end describe '#destroy' do let(:article) do MongoidArticle.create!(title: 'Test') end before do article MongoidArticle.create!(title: 'Coding') article.destroy MongoidArticle.__elasticsearch__.refresh_index! end it 'removes documents from the index' do expect(MongoidArticle.search('title:test').results.total).to eq(0) expect(MongoidArticle.search('title:code').results.total).to eq(1) end end describe 'updates to the document' do let(:article) do MongoidArticle.create!(title: 'Test') end before do article.title = 'Writing' article.save MongoidArticle.__elasticsearch__.refresh_index! end it 'indexes updates' do expect(MongoidArticle.search('title:write').results.total).to eq(1) expect(MongoidArticle.search('title:test').results.total).to eq(0) end end describe 'DSL search' do before do MongoidArticle.create! title: 'Test' MongoidArticle.create! title: 'Testing Coding' MongoidArticle.create! title: 'Coding' MongoidArticle.__elasticsearch__.refresh_index! end let(:search_result) do MongoidArticle.search(query: { match: { title: { query: 'test' } } }) end it 'finds the matching documents' do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end end describe 'paging a collection' do before do MongoidArticle.create! title: 'Test' MongoidArticle.create! title: 'Testing Coding' MongoidArticle.create! title: 'Coding' MongoidArticle.__elasticsearch__.refresh_index! end let(:search_result) do MongoidArticle.search(query: { match: { title: { query: 'test' } } }, size: 2, from: 1) end it 'applies the size and from parameters' do expect(search_result.results.size).to eq(1) expect(search_result.results.first.title).to eq('Testing Coding') expect(search_result.records.size).to eq(1) expect(search_result.records.first.title).to eq('Testing Coding') end end describe 'importing' do before do 97.times { |i| MongoidArticle.create! title: "Test #{i}" } MongoidArticle.__elasticsearch__.create_index! force: true MongoidArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' end context 'when there is no default scope' do let!(:batch_count) do batches = 0 errors = MongoidArticle.import(batch_size: 10) do |response| batches += 1 end MongoidArticle.__elasticsearch__.refresh_index! batches end it 'imports all the documents' do expect(MongoidArticle.search('*').results.total).to eq(97) end it 'uses the specified batch size' do expect(batch_count).to eq(10) end end context 'when there is a default scope' do around(:all) do |example| 10.times { |i| MongoidArticle.create! title: 'Test', views: "#{i}" } MongoidArticle.default_scope -> { MongoidArticle.gt(views: 3) } example.run MongoidArticle.default_scoping = nil end before do MongoidArticle.__elasticsearch__.import MongoidArticle.__elasticsearch__.refresh_index! end it 'uses the default scope' do expect(MongoidArticle.search('*').results.total).to eq(6) end end context 'when there is a default scope and a query specified' do around(:all) do |example| 10.times { |i| MongoidArticle.create! title: 'Test', views: "#{i}" } MongoidArticle.default_scope -> { MongoidArticle.gt(views: 3) } example.run MongoidArticle.default_scoping = nil end before do MongoidArticle.import(query: -> { lte(views: 4) }) MongoidArticle.__elasticsearch__.refresh_index! end it 'combines the query and the default scope' do expect(MongoidArticle.search('*').results.total).to eq(1) end end context 'when the batch is empty' do before do MongoidArticle.delete_all MongoidArticle.import MongoidArticle.__elasticsearch__.refresh_index! end it 'does not make any requests to create documents' do expect(MongoidArticle.search('*').results.total).to eq(0) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/0000755000004100000410000000000014217570270026746 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/multi_model_spec.rb0000644000004100000410000001026014217570270032616 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord MultiModel' do before(:all) do ActiveRecord::Schema.define do create_table Episode.table_name do |t| t.string :name t.datetime :created_at, :default => 'NOW()' end create_table Series.table_name do |t| t.string :name t.datetime :created_at, :default => 'NOW()' end end end before do models = [ Episode, Series ] clear_tables(models) models.each do |model| model.__elasticsearch__.create_index! force: true model.create name: "The #{model.name}" model.create name: "A great #{model.name}" model.create name: "The greatest #{model.name}" model.__elasticsearch__.refresh_index! end end after do clear_indices(Episode, Series) clear_tables(Episode, Series) end context 'when the search is across multimodels' do let(:search_result) do Elasticsearch::Model.search(%q<"The greatest Episode"^2 OR "The greatest Series">, [Series, Episode]) end it 'executes the search across models' do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end describe '#results' do it 'returns an instance of Elasticsearch::Model::Response::Result' do expect(search_result.results[0]).to be_a(Elasticsearch::Model::Response::Result) expect(search_result.results[1]).to be_a(Elasticsearch::Model::Response::Result) end it 'returns the correct model instance' do expect(search_result.results[0].name).to eq('The greatest Episode') expect(search_result.results[1].name).to eq('The greatest Series') end it 'provides access to the results' do expect(search_result.results[0].name).to eq('The greatest Episode') expect(search_result.results[0].name?).to be(true) expect(search_result.results[0].boo?).to be(false) expect(search_result.results[1].name).to eq('The greatest Series') expect(search_result.results[1].name?).to be(true) expect(search_result.results[1].boo?).to be(false) end end describe '#records' do it 'returns an instance of Elasticsearch::Model::Response::Result' do expect(search_result.records[0]).to be_a(Episode) expect(search_result.records[1]).to be_a(Series) end it 'returns the correct model instance' do expect(search_result.records[0].name).to eq('The greatest Episode') expect(search_result.records[1].name).to eq('The greatest Series') end context 'when the data store is changed' do before do Series.find_by_name("The greatest Series").delete Series.__elasticsearch__.refresh_index! end it 'only returns matching records' do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(1 ) expect(search_result.records[0].name).to eq('The greatest Episode') end end end describe 'pagination' do let(:search_result) do Elasticsearch::Model.search('series OR episode', [Series, Episode]) end it 'properly paginates the results' do expect(search_result.page(1).per(3).results.size).to eq(3) expect(search_result.page(2).per(3).results.size).to eq(3) expect(search_result.page(3).per(3).results.size).to eq(0) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/associations_spec.rb0000644000004100000410000002360214217570270033007 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Associations' do before(:all) do ActiveRecord::Schema.define(version: 1) do create_table :categories do |t| t.string :title t.timestamps null: false end create_table :categories_posts do |t| t.references :post, :category end create_table :authors do |t| t.string :first_name, :last_name t.timestamps null: false end create_table :authorships do |t| t.string :first_name, :last_name t.references :post t.references :author t.timestamps null: false end create_table :comments do |t| t.string :text t.string :author t.references :post t.timestamps null: false end add_index(:comments, :post_id) unless index_exists?(:comments, :post_id) create_table :posts do |t| t.string :title t.text :text t.boolean :published t.timestamps null: false end end Comment.__send__ :include, Elasticsearch::Model Comment.__send__ :include, Elasticsearch::Model::Callbacks end before do clear_tables(:categories, :categories_posts, :authors, :authorships, :comments, :posts) clear_indices(Post) Post.__elasticsearch__.create_index!(force: true) Comment.__elasticsearch__.create_index!(force: true) end after do clear_tables(Post, Category) clear_indices(Post) end context 'when a document is created' do before do Post.create!(title: 'Test') Post.create!(title: 'Testing Coding') Post.create!(title: 'Coding') Post.__elasticsearch__.refresh_index! end let(:search_result) do Post.search('title:test') end it 'indexes the document' do expect(search_result.results.size).to eq(2) expect(search_result.results.first.title).to eq('Test') expect(search_result.records.size).to eq(2) expect(search_result.records.first.title).to eq('Test') end end describe 'has_many_and_belongs_to association' do context 'when an association is updated' do before do post.categories = [category_a, category_b] Post.__elasticsearch__.refresh_index! end let(:category_a) do Category.where(title: "One").first_or_create! end let(:category_b) do Category.where(title: "Two").first_or_create! end let(:post) do Post.create! title: "First Post", text: "This is the first post..." end let(:search_result) do Post.search(query: { bool: { must: { multi_match: { fields: ['title'], query: 'first' } }, filter: { terms: { categories: ['One'] } } } } ) end it 'applies the update with' do expect(search_result.results.size).to eq(1) expect(search_result.results.first.title).to eq('First Post') expect(search_result.records.size).to eq(1) expect(search_result.records.first.title).to eq('First Post') end end context 'when an association is deleted' do before do post.categories = [category_a, category_b] post.categories = [category_b] Post.__elasticsearch__.refresh_index! end let(:category_a) do Category.where(title: "One").first_or_create! end let(:category_b) do Category.where(title: "Two").first_or_create! end let(:post) do Post.create! title: "First Post", text: "This is the first post..." end let(:search_result) do Post.search(query: { bool: { must: { multi_match: { fields: ['title'], query: 'first' } }, filter: { terms: { categories: ['One'] } } } } ) end it 'applies the update with a reindex' do expect(search_result.results.size).to eq(0) expect(search_result.records.size).to eq(0) end end end describe 'has_many through association' do context 'when the association is updated' do before do author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create! author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create! author_c = Author.where(first_name: "Kobe", last_name: "Griss").first_or_create! # Create posts post_1 = Post.create!(title: "First Post", text: "This is the first post...") post_2 = Post.create!(title: "Second Post", text: "This is the second post...") post_3 = Post.create!(title: "Third Post", text: "This is the third post...") # Assign authors post_1.authors = [author_a, author_b] post_2.authors = [author_a] post_3.authors = [author_c] Post.__elasticsearch__.refresh_index! end context 'if active record is at least 4' do let(:search_result) do Post.search('authors.full_name:john') end it 'applies the update', if: active_record_at_least_4? do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end end context 'if active record is less than 4' do let(:search_result) do Post.search('authors.author.full_name:john') end it 'applies the update', if: !active_record_at_least_4? do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end end end context 'when an association is added', if: active_record_at_least_4? do before do author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create! author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create! # Create posts post_1 = Post.create!(title: "First Post", text: "This is the first post...") # Assign authors post_1.authors = [author_a] post_1.authors << author_b Post.__elasticsearch__.refresh_index! end let(:search_result) do Post.search('authors.full_name:john') end it 'adds the association' do expect(search_result.results.size).to eq(1) expect(search_result.records.size).to eq(1) end end end describe 'has_many association' do context 'when an association is added', if: active_record_at_least_4? do before do # Create posts post_1 = Post.create!(title: "First Post", text: "This is the first post...") post_2 = Post.create!(title: "Second Post", text: "This is the second post...") # Add comments post_1.comments.create!(author: 'John', text: 'Excellent') post_1.comments.create!(author: 'Abby', text: 'Good') post_2.comments.create!(author: 'John', text: 'Terrible') post_1.comments.create!(author: 'John', text: 'Or rather just good...') Post.__elasticsearch__.refresh_index! end let(:search_result) do Post.search(query: { nested: { path: 'comments', query: { bool: { must: [ { match: { 'comments.author' => 'john' } }, { match: { 'comments.text' => 'good' } } ] } } } }) end it 'adds the association' do expect(search_result.results.size).to eq(1) end end end describe '#touch' do context 'when a touch callback is defined on the model' do before do # Create categories category_a = Category.where(title: "One").first_or_create! # Create post post = Post.create!(title: "First Post", text: "This is the first post...") # Assign category post.categories << category_a category_a.update_attribute(:title, "Updated") category_a.posts.each { |p| p.touch } Post.__elasticsearch__.refresh_index! end it 'executes the callback after #touch' do expect(Post.search('categories:One').size).to eq(0) expect(Post.search('categories:Updated').size).to eq(1) end end end describe '#includes' do before do post_1 = Post.create(title: 'One') post_2 = Post.create(title: 'Two') post_1.comments.create(text: 'First comment') post_2.comments.create(text: 'Second comment') Comment.__elasticsearch__.refresh_index! end let(:search_result) do Comment.search('first').records(includes: :post) end it 'eager loads associations' do expect(search_result.first.association(:post)).to be_loaded expect(search_result.first.post.title).to eq('One') end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/basic_spec.rb0000644000004100000410000002767014217570270031402 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter::ActiveRecord do context 'when a document_type is not defined for the Model' do before do ActiveRecord::Schema.define(:version => 1) do create_table :article_no_types do |t| t.string :title t.string :body t.integer :clicks, :default => 0 t.datetime :created_at, :default => 'NOW()' end end ArticleNoType.delete_all ArticleNoType.__elasticsearch__.create_index!(force: true) ArticleNoType.create!(title: 'Test', body: '', clicks: 1) ArticleNoType.create!(title: 'Testing Coding', body: '', clicks: 2) ArticleNoType.create!(title: 'Coding', body: '', clicks: 3) ArticleNoType.__elasticsearch__.refresh_index! end describe 'indexing a document' do let(:search_result) do ArticleNoType.search('title:test') end it 'allows searching for documents' do expect(search_result.results.size).to be(2) expect(search_result.records.size).to be(2) end end end context 'when a document_type is defined for the Model' do before(:all) do ActiveRecord::Schema.define(:version => 1) do create_table :articles do |t| t.string :title t.string :body t.integer :clicks, :default => 0 t.datetime :created_at, :default => 'NOW()' end end Article.delete_all Article.__elasticsearch__.create_index!(force: true, include_type_name: true) Article.create!(title: 'Test', body: '', clicks: 1) Article.create!(title: 'Testing Coding', body: '', clicks: 2) Article.create!(title: 'Coding', body: '', clicks: 3) Article.__elasticsearch__.refresh_index! end describe 'indexing a document' do let(:search_result) do Article.search('title:test') end it 'allows searching for documents' do expect(search_result.results.size).to be(2) expect(search_result.records.size).to be(2) end end describe '#results' do let(:search_result) do Article.search('title:test') end it 'returns an instance of Response::Result' do expect(search_result.results.first).to be_a(Elasticsearch::Model::Response::Result) end it 'prooperly loads the document' do expect(search_result.results.first.title).to eq('Test') end context 'when the result contains other data' do let(:search_result) do Article.search(query: { match: { title: 'test' } }, highlight: { fields: { title: {} } }) end it 'allows access to the Elasticsearch result' do expect(search_result.results.first.title).to eq('Test') expect(search_result.results.first.title?).to be(true) expect(search_result.results.first.boo?).to be(false) expect(search_result.results.first.highlight?).to be(true) expect(search_result.results.first.highlight.title?).to be(true) expect(search_result.results.first.highlight.boo?).to be(false) end end end describe '#records' do let(:search_result) do Article.search('title:test') end it 'returns an instance of the model' do expect(search_result.records.first).to be_a(Article) end it 'prooperly loads the document' do expect(search_result.records.first.title).to eq('Test') end end describe 'Enumerable' do let(:search_result) do Article.search('title:test') end it 'allows iteration over results' do expect(search_result.results.map(&:_id)).to eq(['1', '2']) end it 'allows iteration over records' do expect(search_result.records.map(&:id)).to eq([1, 2]) end end describe '#id' do let(:search_result) do Article.search('title:test') end it 'returns the id' do expect(search_result.results.first.id).to eq('1') end end describe '#id' do let(:search_result) do Article.search('title:test') end it 'returns the type' do expect(search_result.results.first.type).to eq('article') end end describe '#each_with_hit' do let(:search_result) do Article.search('title:test') end it 'returns the record with the Elasticsearch hit' do search_result.records.each_with_hit do |r, h| expect(h._score).not_to be_nil expect(h._source.title).not_to be_nil end end end describe 'search results order' do let(:search_result) do Article.search(query: { match: { title: 'code' }}, sort: { clicks: :desc }) end it 'preserves the search results order when accessing a single record' do expect(search_result.records[0].clicks).to be(3) expect(search_result.records[1].clicks).to be(2) expect(search_result.records.first).to eq(search_result.records[0]) end it 'preserves the search results order for the list of records' do search_result.records.each_with_hit do |r, h| expect(r.id.to_s).to eq(h._id) end search_result.records.map_with_hit do |r, h| expect(r.id.to_s).to eq(h._id) end end end describe 'a paged collection' do let(:search_result) do Article.search(query: { match: { title: { query: 'test' } } }, size: 2, from: 1) end it 'applies the paged options to the search' do expect(search_result.results.size).to eq(1) expect(search_result.results.first.title).to eq('Testing Coding') expect(search_result.records.size).to eq(1) expect(search_result.records.first.title).to eq('Testing Coding') end end describe '#destroy' do before do Article.create!(title: 'destroy', body: '', clicks: 1) Article.__elasticsearch__.refresh_index! Article.where(title: 'destroy').first.destroy Article.__elasticsearch__.refresh_index! end let(:search_result) do Article.search('title:test') end it 'removes the document from the index' do expect(Article.count).to eq(3) expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end end describe 'full document updates' do before do article = Article.create!(title: 'update', body: '', clicks: 1) Article.__elasticsearch__.refresh_index! article.title = 'Writing' article.save Article.__elasticsearch__.refresh_index! end let(:search_result) do Article.search('title:write') end it 'applies the update' do expect(search_result.results.size).to eq(1) expect(search_result.records.size).to eq(1) end end describe 'attribute updates' do before do article = Article.create!(title: 'update', body: '', clicks: 1) Article.__elasticsearch__.refresh_index! article.title = 'special' article.save Article.__elasticsearch__.refresh_index! end let(:search_result) do Article.search('title:special') end it 'applies the update' do expect(search_result.results.size).to eq(1) expect(search_result.records.size).to eq(1) end end describe '#save' do before do article = Article.create!(title: 'save', body: '', clicks: 1) ActiveRecord::Base.transaction do article.body = 'dummy' article.save article.title = 'special' article.save end article.__elasticsearch__.update_document Article.__elasticsearch__.refresh_index! end let(:search_result) do Article.search('body:dummy') end it 'applies the save' do expect(search_result.results.size).to eq(1) expect(search_result.records.size).to eq(1) end end describe 'a DSL search' do let(:search_result) do Article.search(query: { match: { title: { query: 'test' } } }) end it 'returns the results' do expect(search_result.results.size).to eq(2) expect(search_result.records.size).to eq(2) end end describe 'chaining SQL queries on response.records' do let(:search_result) do Article.search(query: { match: { title: { query: 'test' } } }) end it 'executes the SQL request with the chained query criteria' do expect(search_result.records.size).to eq(2) expect(search_result.records.where(title: 'Test').size).to eq(1) expect(search_result.records.where(title: 'Test').first.title).to eq('Test') end end describe 'ordering of SQL queries' do context 'when order is called on the ActiveRecord query' do let(:search_result) do Article.search query: { match: { title: { query: 'test' } } } end it 'allows the SQL query to be ordered independent of the Elasticsearch results order', unless: active_record_at_least_4? do expect(search_result.records.order('title DESC').first.title).to eq('Testing Coding') expect(search_result.records.order('title DESC')[0].title).to eq('Testing Coding') end it 'allows the SQL query to be ordered independent of the Elasticsearch results order', if: active_record_at_least_4? do expect(search_result.records.order(title: :desc).first.title).to eq('Testing Coding') expect(search_result.records.order(title: :desc)[0].title).to eq('Testing Coding') end end context 'when more methods are chained on the ActiveRecord query' do let(:search_result) do Article.search query: {match: {title: {query: 'test'}}} end it 'allows the SQL query to be ordered independent of the Elasticsearch results order', if: active_record_at_least_4? do expect(search_result.records.distinct.order(title: :desc).first.title).to eq('Testing Coding') expect(search_result.records.distinct.order(title: :desc)[0].title).to eq('Testing Coding') end end end describe 'access to the response via methods' do let(:search_result) do Article.search(query: { match: { title: { query: 'test' } } }, aggregations: { dates: { date_histogram: { field: 'created_at', interval: 'hour' } }, clicks: { global: {}, aggregations: { min: { min: { field: 'clicks' } } } } }, suggest: { text: 'tezt', title: { term: { field: 'title', suggest_mode: 'always' } } }) end it 'allows document keys to be access via methods' do expect(search_result.aggregations.dates.buckets.first.doc_count).to eq(2) expect(search_result.aggregations.clicks.doc_count).to eq(6) expect(search_result.aggregations.clicks.min.value).to eq(1.0) expect(search_result.aggregations.clicks.max).to be_nil expect(search_result.suggestions.title.first.options.size).to eq(1) expect(search_result.suggestions.terms).to eq(['test']) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/import_spec.rb0000644000004100000410000001403614217570270031623 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Importing' do before(:all) do ActiveRecord::Schema.define(:version => 1) do create_table :import_articles do |t| t.string :title t.integer :views t.string :numeric # For the sake of invalid data sent to Elasticsearch t.datetime :created_at, :default => 'NOW()' end end ImportArticle.delete_all ImportArticle.__elasticsearch__.client.cluster.health(wait_for_status: 'yellow') end before do ImportArticle.__elasticsearch__.create_index! end after do clear_indices(ImportArticle) clear_tables(ImportArticle) end describe '#import' do context 'when no search criteria is specified' do before do 10.times { |i| ImportArticle.create! title: 'Test', views: i.to_s } ImportArticle.import ImportArticle.__elasticsearch__.refresh_index! end it 'imports all documents' do expect(ImportArticle.search('*').results.total).to eq(10) end end context 'when batch size is specified' do before do 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } end let!(:batch_count) do batches = 0 errors = ImportArticle.import(batch_size: 5) do |response| batches += 1 end ImportArticle.__elasticsearch__.refresh_index! batches end it 'imports using the batch size' do expect(batch_count).to eq(2) end it 'imports all the documents' do expect(ImportArticle.search('*').results.total).to eq(10) end end context 'when a scope is specified' do before do 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } ImportArticle.import(scope: 'popular', force: true) ImportArticle.__elasticsearch__.refresh_index! end it 'applies the scope' do expect(ImportArticle.search('*').results.total).to eq(5) end end context 'when a query is specified' do before do 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } ImportArticle.import(query: -> { where('views >= 3') }) ImportArticle.__elasticsearch__.refresh_index! end it 'applies the query' do expect(ImportArticle.search('*').results.total).to eq(7) end end context 'when there are invalid documents' do let!(:result) do 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } new_article batches = 0 errors = ImportArticle.__elasticsearch__.import(batch_size: 5) do |response| batches += 1 end ImportArticle.__elasticsearch__.refresh_index! { batch_size: batches, errors: errors} end let(:new_article) do ImportArticle.create!(title: "Test INVALID", numeric: "INVALID") end it 'does not import them' do expect(ImportArticle.search('*').results.total).to eq(10) expect(result[:batch_size]).to eq(3) expect(result[:errors]).to eq(1) end end context 'when a transform proc is specified' do before do 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } ImportArticle.import( transform: ->(a) {{ index: { data: { name: a.title, foo: 'BAR' } }}} ) ImportArticle.__elasticsearch__.refresh_index! end it 'transforms the documents' do expect(ImportArticle.search('*').results.first._source.keys).to include('name') expect(ImportArticle.search('*').results.first._source.keys).to include('foo') end it 'imports all documents' do expect(ImportArticle.search('test').results.total).to eq(10) expect(ImportArticle.search('bar').results.total).to eq(10) end end context 'when the model has a default scope' do around(:all) do |example| 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } ImportArticle.instance_eval { default_scope { where('views > 3') } } example.run ImportArticle.default_scopes.pop end before do ImportArticle.__elasticsearch__.import ImportArticle.__elasticsearch__.refresh_index! end it 'uses the default scope' do expect(ImportArticle.search('*').results.total).to eq(6) end end context 'when there is a default scope and a query specified' do around(:all) do |example| 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } ImportArticle.instance_eval { default_scope { where('views > 3') } } example.run ImportArticle.default_scopes.pop end before do ImportArticle.import(query: -> { where('views <= 4') }) ImportArticle.__elasticsearch__.refresh_index! end it 'combines the query and the default scope' do expect(ImportArticle.search('*').results.total).to eq(1) end end context 'when the batch is empty' do before do ImportArticle.delete_all ImportArticle.import ImportArticle.__elasticsearch__.refresh_index! end it 'does not make any requests to create documents' do expect(ImportArticle.search('*').results.total).to eq(0) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/dynamic_index_name_spec.rb0000644000004100000410000000246614217570270034130 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Dynamic Index naming' do before do ArticleWithDynamicIndexName.counter = 0 end it 'exavlues the index_name value' do expect(ArticleWithDynamicIndexName.index_name).to eq('articles-1') end it 'revaluates the index name with each call' do expect(ArticleWithDynamicIndexName.index_name).to eq('articles-1') expect(ArticleWithDynamicIndexName.index_name).to eq('articles-2') expect(ArticleWithDynamicIndexName.index_name).to eq('articles-3') end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/namespaced_model_spec.rb0000644000004100000410000000346314217570270033573 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Namespaced Model' do before(:all) do ActiveRecord::Schema.define(:version => 1) do create_table :books do |t| t.string :title end end MyNamespace::Book.delete_all MyNamespace::Book.__elasticsearch__.create_index!(force: true, include_type_name: true) MyNamespace::Book.create!(title: 'Test') MyNamespace::Book.__elasticsearch__.refresh_index! end after do clear_indices(MyNamespace::Book) clear_tables(MyNamespace::Book) end context 'when the model is namespaced' do it 'has the proper index name' do expect(MyNamespace::Book.index_name).to eq('my_namespace-books') end it 'has the proper document type' do expect(MyNamespace::Book.document_type).to eq('book') end it 'saves the document into the index' do expect(MyNamespace::Book.search('title:test').results.size).to eq(1) expect(MyNamespace::Book.search('title:test').results.first.title).to eq('Test') end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/parent_child_spec.rb0000644000004100000410000000555114217570270032747 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Parent-Child' do before(:all) do ActiveRecord::Schema.define(version: 1) do create_table :questions do |t| t.string :title t.text :text t.string :author t.timestamps null: false end create_table :answers do |t| t.text :text t.string :author t.references :question t.timestamps null: false end add_index(:answers, :question_id) unless index_exists?(:answers, :question_id) clear_tables(Question) ParentChildSearchable.create_index!(force: true, include_type_name: true) q_1 = Question.create!(title: 'First Question', author: 'John') q_2 = Question.create!(title: 'Second Question', author: 'Jody') q_1.answers.create!(text: 'Lorem Ipsum', author: 'Adam') q_1.answers.create!(text: 'Dolor Sit', author: 'Ryan') q_2.answers.create!(text: 'Amet Et', author: 'John') Question.__elasticsearch__.refresh_index! end end describe 'has_child search' do let(:search_result) do Question.search(query: { has_child: { type: 'answer', query: { match: { author: 'john' } } } }) end it 'finds parents by matching on child search criteria' do expect(search_result.records.first.title).to eq('Second Question') end end describe 'hash_parent search' do let(:search_result) do Answer.search(query: { has_parent: { parent_type: 'question', query: { match: { author: 'john' } } } }) end it 'finds children by matching in parent criteria' do expect(search_result.records.map(&:author)).to match(['Adam', 'Ryan']) end end context 'when a parent is deleted' do before do Question.where(title: 'First Question').each(&:destroy) Question.__elasticsearch__.refresh_index! end let(:search_result) do Answer.search(query: { has_parent: { parent_type: 'question', query: { match_all: {} } } }) end it 'deletes the children' do expect(search_result.results.total).to eq(1) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/pagination_spec.rb0000644000004100000410000001721314217570270032442 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Pagination' do before(:all) do ActiveRecord::Schema.define(:version => 1) do create_table ArticleForPagination.table_name do |t| t.string :title t.datetime :created_at, :default => 'NOW()' t.boolean :published end end Kaminari::Hooks.init if defined?(Kaminari::Hooks) ArticleForPagination.__elasticsearch__.create_index! force: true 68.times do |i| ArticleForPagination.create! title: "Test #{i}", published: (i % 2 == 0) end ArticleForPagination.import ArticleForPagination.__elasticsearch__.refresh_index! end context 'when no other page is specified' do let(:records) do ArticleForPagination.search('title:test').page(1).records end describe '#size' do it 'returns the correct size' do expect(records.size).to eq(25) end end describe '#current_page' do it 'returns the correct current page' do expect(records.current_page).to eq(1) end end describe '#prev_page' do it 'returns the correct previous page' do expect(records.prev_page).to be_nil end end describe '#next_page' do it 'returns the correct next page' do expect(records.next_page).to eq(2) end end describe '#total_pages' do it 'returns the correct total pages' do expect(records.total_pages).to eq(3) end end describe '#first_page?' do it 'returns the correct first page' do expect(records.first_page?).to be(true) end end describe '#last_page?' do it 'returns the correct last page' do expect(records.last_page?).to be(false) end end describe '#out_of_range?' do it 'returns whether the pagination is out of range' do expect(records.out_of_range?).to be(false) end end end context 'when a specific page is specified' do let(:records) do ArticleForPagination.search('title:test').page(2).records end describe '#size' do it 'returns the correct size' do expect(records.size).to eq(25) end end describe '#current_page' do it 'returns the correct current page' do expect(records.current_page).to eq(2) end end describe '#prev_page' do it 'returns the correct previous page' do expect(records.prev_page).to eq(1) end end describe '#next_page' do it 'returns the correct next page' do expect(records.next_page).to eq(3) end end describe '#total_pages' do it 'returns the correct total pages' do expect(records.total_pages).to eq(3) end end describe '#first_page?' do it 'returns the correct first page' do expect(records.first_page?).to be(false) end end describe '#last_page?' do it 'returns the correct last page' do expect(records.last_page?).to be(false) end end describe '#out_of_range?' do it 'returns whether the pagination is out of range' do expect(records.out_of_range?).to be(false) end end end context 'when a the last page is specified' do let(:records) do ArticleForPagination.search('title:test').page(3).records end describe '#size' do it 'returns the correct size' do expect(records.size).to eq(18) end end describe '#current_page' do it 'returns the correct current page' do expect(records.current_page).to eq(3) end end describe '#prev_page' do it 'returns the correct previous page' do expect(records.prev_page).to eq(2) end end describe '#next_page' do it 'returns the correct next page' do expect(records.next_page).to be_nil end end describe '#total_pages' do it 'returns the correct total pages' do expect(records.total_pages).to eq(3) end end describe '#first_page?' do it 'returns the correct first page' do expect(records.first_page?).to be(false) end end describe '#last_page?' do it 'returns the correct last page' do expect(records.last_page?).to be(true) end end describe '#out_of_range?' do it 'returns whether the pagination is out of range' do expect(records.out_of_range?).to be(false) end end end context 'when an invalid page is specified' do let(:records) do ArticleForPagination.search('title:test').page(6).records end describe '#size' do it 'returns the correct size' do expect(records.size).to eq(0) end end describe '#current_page' do it 'returns the correct current page' do expect(records.current_page).to eq(6) end end describe '#next_page' do it 'returns the correct next page' do expect(records.next_page).to be_nil end end describe '#total_pages' do it 'returns the correct total pages' do expect(records.total_pages).to eq(3) end end describe '#first_page?' do it 'returns the correct first page' do expect(records.first_page?).to be(false) end end describe '#last_page?' do it 'returns whether it is the last page', if: !(Kaminari::VERSION < '1') do expect(records.last_page?).to be(false) end it 'returns whether it is the last page', if: Kaminari::VERSION < '1' do expect(records.last_page?).to be(true) # Kaminari returns current_page >= total_pages in version < 1.0 end end describe '#out_of_range?' do it 'returns whether the pagination is out of range' do expect(records.out_of_range?).to be(true) end end end context 'when a scope is also specified' do let(:records) do ArticleForPagination.search('title:test').page(2).records.published end describe '#size' do it 'returns the correct size' do expect(records.size).to eq(12) end end end context 'when a sorting is specified' do let(:search) do ArticleForPagination.search({ query: { match: { title: 'test' } }, sort: [ { id: 'desc' } ] }) end it 'applies the sort' do expect(search.page(2).records.first.id).to eq(43) expect(search.page(3).records.first.id).to eq(18) expect(search.page(2).per(5).records.first.id).to eq(63) end end context 'when the model has a specific default per page set' do around do |example| original_default = ArticleForPagination.instance_variable_get(:@_default_per_page) ArticleForPagination.paginates_per 50 example.run ArticleForPagination.paginates_per original_default end it 'uses the default per page setting' do expect(ArticleForPagination.search('*').page(1).records.size).to eq(50) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapters/active_record/serialization_spec.rb0000644000004100000410000000542714217570270033172 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Adapter::ActiveRecord Serialization' do before(:all) do ActiveRecord::Schema.define(:version => 1) do create_table ArticleWithCustomSerialization.table_name do |t| t.string :title t.string :status end end ArticleWithCustomSerialization.delete_all ArticleWithCustomSerialization.__elasticsearch__.create_index!(force: true) end context 'when the model has a custom serialization defined' do before do ArticleWithCustomSerialization.create!(title: 'Test', status: 'green') ArticleWithCustomSerialization.__elasticsearch__.refresh_index! end context 'when a document is indexed' do let(:search_result) do ArticleWithCustomSerialization.__elasticsearch__.client.get(index: 'article_with_custom_serializations', type: '_doc', id: '1') end it 'applies the serialization when indexing' do expect(search_result['_source']).to eq('title' => 'Test') end end context 'when a document is updated' do before do article.update_attributes(title: 'UPDATED', status: 'yellow') ArticleWithCustomSerialization.__elasticsearch__.refresh_index! end let!(:article) do art = ArticleWithCustomSerialization.create!(title: 'Test', status: 'red') ArticleWithCustomSerialization.__elasticsearch__.refresh_index! art end let(:search_result) do ArticleWithCustomSerialization.__elasticsearch__.client.get(index: 'article_with_custom_serializations', type: '_doc', id: article.id) end it 'applies the serialization when updating' do expect(search_result['_source']).to eq('title' => 'UPDATED') end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/adapter_spec.rb0000644000004100000410000000772114217570270025320 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Adapter do before(:all) do class ::DummyAdapterClass; end class ::DummyAdapterClassWithAdapter; end class ::DummyAdapter Records = Module.new Callbacks = Module.new Importing = Module.new end end after(:all) do [DummyAdapterClassWithAdapter, DummyAdapterClass, DummyAdapter].each do |adapter| Elasticsearch::Model::Adapter::Adapter.adapters.delete(adapter) end remove_classes(DummyAdapterClass, DummyAdapterClassWithAdapter, DummyAdapter) end describe '#from_class' do it 'should return an Adapter instance' do expect(Elasticsearch::Model::Adapter.from_class(DummyAdapterClass)).to be_a(Elasticsearch::Model::Adapter::Adapter) end end describe 'register' do before do expect(Elasticsearch::Model::Adapter::Adapter).to receive(:register).and_call_original Elasticsearch::Model::Adapter.register(:foo, lambda { |c| false }) end it 'should register an adapter' do expect(Elasticsearch::Model::Adapter::Adapter.adapters[:foo]).to be_a(Proc) end context 'when a specific adapter class is set' do before do expect(Elasticsearch::Model::Adapter::Adapter).to receive(:register).and_call_original Elasticsearch::Model::Adapter::Adapter.register(DummyAdapter, lambda { |c| c == DummyAdapterClassWithAdapter }) end let(:adapter) do Elasticsearch::Model::Adapter::Adapter.new(DummyAdapterClassWithAdapter) end it 'should register the adapter' do expect(adapter.adapter).to eq(DummyAdapter) end end end describe 'default adapter' do let(:adapter) do Elasticsearch::Model::Adapter::Adapter.new(DummyAdapterClass) end it 'sets a default adapter' do expect(adapter.adapter).to eq(Elasticsearch::Model::Adapter::Default) end end describe '#records_mixin' do before do Elasticsearch::Model::Adapter::Adapter.register(DummyAdapter, lambda { |c| c == DummyAdapterClassWithAdapter }) end let(:adapter) do Elasticsearch::Model::Adapter::Adapter.new(DummyAdapterClassWithAdapter) end it 'returns a Module' do expect(adapter.records_mixin).to be_a(Module) end end describe '#callbacks_mixin' do before do Elasticsearch::Model::Adapter::Adapter.register(DummyAdapter, lambda { |c| c == DummyAdapterClassWithAdapter }) end let(:adapter) do Elasticsearch::Model::Adapter::Adapter.new(DummyAdapterClassWithAdapter) end it 'returns a Module' do expect(adapter.callbacks_mixin).to be_a(Module) end end describe '#importing_mixin' do before do Elasticsearch::Model::Adapter::Adapter.register(DummyAdapter, lambda { |c| c == DummyAdapterClassWithAdapter }) end let(:adapter) do Elasticsearch::Model::Adapter::Adapter.new(DummyAdapterClassWithAdapter) end it 'returns a Module' do expect(adapter.importing_mixin).to be_a(Module) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/searching_spec.rb0000644000004100000410000000373414217570270025643 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Searching::ClassMethods do before(:all) do class ::DummySearchingModel extend Elasticsearch::Model::Searching::ClassMethods def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(DummySearchingModel) end it 'has the search method' do expect(DummySearchingModel).to respond_to(:search) end describe '#search' do let(:response) do double('search', execute!: { 'hits' => {'hits' => [ {'_id' => 2 }, {'_id' => 1 } ]} }) end before do expect(Elasticsearch::Model::Searching::SearchRequest).to receive(:new).with(DummySearchingModel, 'foo', { default_operator: 'AND' }).and_return(response) end it 'creates a search object' do expect(DummySearchingModel.search('foo', default_operator: 'AND')).to be_a(Elasticsearch::Model::Response::Response) end end describe 'lazy execution' do let(:response) do double('search').tap do |r| expect(r).to receive(:execute!).never end end it 'does not execute the search until the results are accessed' do DummySearchingModel.search('foo') end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/0000755000004100000410000000000014217570270024170 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/response/base_spec.rb0000644000004100000410000000514514217570270026446 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Response::Base do before(:all) do class DummyBaseClass include Elasticsearch::Model::Response::Base end class OriginClass def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(DummyBaseClass, OriginClass) end let(:response_document) do { 'hits' => { 'total' => 123, 'max_score' => 456, 'hits' => [] } } end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(OriginClass, '*').tap do |request| allow(request).to receive(:execute!).and_return(response_document) end end let(:response) do Elasticsearch::Model::Response::Response.new(OriginClass, search) end let(:response_base) do DummyBaseClass.new(OriginClass, response) end describe '#klass' do it 'returns the class' do expect(response.klass).to be(OriginClass) end end describe '#response' do it 'returns the response object' do expect(response_base.response).to eq(response) end end describe 'response document' do it 'returns the response document' do expect(response_base.response.response).to eq(response_document) end end describe '#total' do it 'returns the total' do expect(response_base.total).to eq(123) end end describe '#max_score' do it 'returns the total' do expect(response_base.max_score).to eq(456) end end describe '#results' do it 'raises a NotImplemented error' do expect { response_base.results }.to raise_exception(Elasticsearch::Model::NotImplemented) end end describe '#records' do it 'raises a NotImplemented error' do expect { response_base.records }.to raise_exception(Elasticsearch::Model::NotImplemented) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/aggregations_spec.rb0000644000004100000410000000441614217570270030206 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Response::Aggregations do before(:all) do class OriginClass def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(OriginClass) end let(:response_document) do { 'aggregations' => { 'foo' => {'bar' => 10 }, 'price' => { 'doc_count' => 123, 'min' => { 'value' => 1.0}, 'max' => { 'value' => 99 } } } } end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(OriginClass, '*').tap do |request| allow(request).to receive(:execute!).and_return(response_document) end end let(:aggregations) do Elasticsearch::Model::Response::Response.new(OriginClass, search).aggregations end describe 'method delegation' do it 'delegates methods to the response document' do expect(aggregations.foo).to be_a(Hashie::Mash) expect(aggregations.foo.bar).to be(10) end end describe '#doc_count' do it 'returns the doc count value from the response document' do expect(aggregations.price.doc_count).to eq(123) end end describe '#min' do it 'returns the min value from the response document' do expect(aggregations.price.min.value).to eq(1) end end describe '#max' do it 'returns the max value from the response document' do expect(aggregations.price.max.value).to eq(99) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/results_spec.rb0000644000004100000410000000437414217570270027240 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Response::Results do before(:all) do class OriginClass def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(OriginClass) end let(:response_document) do { 'hits' => { 'total' => 123, 'max_score' => 456, 'hits' => [{'foo' => 'bar'}] } } end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(OriginClass, '*').tap do |request| allow(request).to receive(:execute!).and_return(response_document) end end let(:response) do Elasticsearch::Model::Response::Response.new(OriginClass, search) end let(:results) do response.results end let(:records) do response.records end describe '#results' do it 'provides access to the results' do expect(results.results.size).to be(1) expect(results.results.first.foo).to eq('bar') end end describe 'Enumerable' do it 'deletebates enumerable methods to the results' do expect(results.empty?).to be(false) expect(results.first.foo).to eq('bar') end end describe '#raw_response' do it 'returns the raw response document' do expect(response.raw_response).to eq(response_document) end end describe '#records' do it 'provides access to the records' do expect(results.records.size).to be(results.results.size) expect(results.records.first.foo).to eq(results.results.first.foo) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/pagination/0000755000004100000410000000000014217570270026321 5ustar www-datawww-dataelasticsearch-model-7.2.1/spec/elasticsearch/model/response/pagination/kaminari_spec.rb0000644000004100000410000003114014217570270031452 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Response::Response Kaminari' do before(:all) do class ModelClass include ::Kaminari::ConfigurationMethods def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(ModelClass) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(model, '*') end let(:response) do allow(model).to receive(:client).and_return(client) Elasticsearch::Model::Response::Response.new(model, search, response_document).tap do |resp| allow(resp).to receive(:client).and_return(client) end end let(:client) do double('client') end shared_examples_for 'a search request that can be paginated' do describe '#page' do it 'does not set an initial from and size on the search definition' do expect(response.search.definition[:from]).to be(nil) expect(response.search.definition[:size]).to be(nil) end context 'when page is called once' do let(:search_request) do { index: index_field, from: 25, size: 25, q: '*', type: type_field} end before do expect(client).to receive(:search).with(search_request).and_return(response_document) response.page(2).to_a end it 'advances the from/size in the search request' do expect(response.search.definition[:from]).to be(25) expect(response.search.definition[:size]).to be(25) end end context 'when page is called more than once' do let(:search_request_one) do { index: index_field, from: 25, size: 25, q: '*', type: type_field} end let(:search_request_two) do { index: index_field, from: 75, size: 25, q: '*', type: type_field} end before do expect(client).to receive(:search).with(search_request_one).and_return(response_document) response.page(2).to_a expect(client).to receive(:search).with(search_request_two).and_return(response_document) response.page(4).to_a end it 'advances the from/size in the search request' do expect(response.search.definition[:from]).to be(75) expect(response.search.definition[:size]).to be(25) end end context 'when limit is also set' do before do response.records response.results end context 'when page is called before limit' do before do response.page(3).limit(35) end it 'sets the correct values' do expect(response.search.definition[:size]).to eq(35) expect(response.search.definition[:from]).to eq(70) end it 'resets the instance variables' do expect(response.instance_variable_get(:@response)).to be(nil) expect(response.instance_variable_get(:@records)).to be(nil) expect(response.instance_variable_get(:@results)).to be(nil) end end context 'when limit is called before page' do before do response.limit(35).page(3) end it 'sets the correct values' do expect(response.search.definition[:size]).to eq(35) expect(response.search.definition[:from]).to eq(70) end it 'resets the instance variables' do expect(response.instance_variable_get(:@response)).to be(nil) expect(response.instance_variable_get(:@records)).to be(nil) expect(response.instance_variable_get(:@results)).to be(nil) end end end end describe '#limit_value' do context 'when there is no default set' do it 'uses the limit value from the Kaminari configuration' do expect(response.limit_value).to eq(Kaminari.config.default_per_page) end end context 'when there is a limit in the search definition' do let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(model, '*', size: 10) end it 'gets the limit from the search definition' do expect(response.limit_value).to eq(10) end end context 'when there is a limit in the search body' do let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(model, { query: { match_all: {} }, size: 999 }) end it 'does not use the limit' do expect(response.limit_value).to be(Kaminari.config.default_per_page) end end end describe '#offset_value' do context 'when there is no default set' do it 'uses an offset of 0' do expect(response.offset_value).to eq(0) end end context 'when there is an offset in the search definition' do let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(model, '*', from: 50) end it 'gets the limit from the search definition' do expect(response.offset_value).to eq(50) end end context 'when there is an offset in the search body' do let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(model, { query: { match_all: {} }, from: 333 }) end it 'does not use the offset' do expect(response.offset_value).to be(0) end end end describe '#limit' do context 'when a limit is set' do before do response.records response.results response.limit(35) end it 'sets the limit on the search defintiion' do expect(response.search.definition[:size]).to eq(35) end it 'resets the instance variables' do expect(response.instance_variable_get(:@response)).to be(nil) expect(response.instance_variable_get(:@records)).to be(nil) expect(response.instance_variable_get(:@results)).to be(nil) end context 'when the limit is provided as a string' do before do response.limit('35') end it 'coerces the string to an integer' do expect(response.search.definition[:size]).to eq(35) end end context 'when the limit is an invalid type' do before do response.limit('asdf') end it 'does not apply the setting' do expect(response.search.definition[:size]).to eq(35) end end end end describe '#offset' do context 'when an offset is set' do before do response.records response.results response.offset(15) end it 'sets the limit on the search defintiion' do expect(response.search.definition[:from]).to eq(15) end it 'resets the instance variables' do expect(response.instance_variable_get(:@response)).to be(nil) expect(response.instance_variable_get(:@records)).to be(nil) expect(response.instance_variable_get(:@results)).to be(nil) end context 'when the offset is provided as a string' do before do response.offset('15') end it 'coerces the string to an integer' do expect(response.search.definition[:from]).to eq(15) end end context 'when the offset is an invalid type' do before do response.offset('asdf') end it 'does not apply the setting' do expect(response.search.definition[:from]).to eq(0) end end end end describe '#total' do before do allow(response.results).to receive(:total).and_return(100) end it 'returns the total number of hits' do expect(response.total_count).to eq(100) end end context 'results' do before do allow(search).to receive(:execute!).and_return(response_document) end describe '#current_page' do it 'returns the current page' do expect(response.results.current_page).to eq(1) end context 'when a particular page is accessed' do it 'returns the correct current page' do expect(response.page(5).results.current_page).to eq(5) end end end describe '#prev_page' do it 'returns the previous page' do expect(response.page(1).results.prev_page).to be(nil) expect(response.page(2).results.prev_page).to be(1) expect(response.page(3).results.prev_page).to be(2) expect(response.page(4).results.prev_page).to be(3) end end describe '#next_page' do it 'returns the previous page' do expect(response.page(1).results.next_page).to be(2) expect(response.page(2).results.next_page).to be(3) expect(response.page(3).results.next_page).to be(4) expect(response.page(4).results.next_page).to be(nil) end end end context 'records' do before do allow(search).to receive(:execute!).and_return(response_document) end describe '#current_page' do it 'returns the current page' do expect(response.records.current_page).to eq(1) end context 'when a particular page is accessed' do it 'returns the correct current page' do expect(response.page(5).records.current_page).to eq(5) end end end describe '#prev_page' do it 'returns the previous page' do expect(response.page(1).records.prev_page).to be(nil) expect(response.page(2).records.prev_page).to be(1) expect(response.page(3).records.prev_page).to be(2) expect(response.page(4).records.prev_page).to be(3) end end describe '#next_page' do it 'returns the previous page' do expect(response.page(1).records.next_page).to be(2) expect(response.page(2).records.next_page).to be(3) expect(response.page(3).records.next_page).to be(4) expect(response.page(4).records.next_page).to be(nil) end end end end context 'when Elasticsearch version is < 7.0' do let(:response_document) do { 'took' => '5', 'timed_out' => false, '_shards' => {'one' => 'OK'}, 'hits' => { 'total' => 100, 'hits' => (1..100).to_a.map { |i| { _id: i } } } } end context 'when the model is a single one' do let(:model) do ModelClass end let(:type_field) do 'bar' end let(:index_field) do 'foo' end it_behaves_like 'a search request that can be paginated' end context 'when the model is a multimodel' do let(:model) do Elasticsearch::Model::Multimodel.new(ModelClass) end let(:type_field) do ['bar'] end let(:index_field) do ['foo'] end it_behaves_like 'a search request that can be paginated' end end context 'when Elasticsearch version is >= 7.0' do let(:response_document) do { 'took' => '5', 'timed_out' => false, '_shards' => {'one' => 'OK'}, 'hits' => { 'total' => { 'value' => 100, 'relation' => 'eq' }, 'hits' => (1..100).to_a.map { |i| { _id: i } } } } end context 'when the model is a single one' do let(:model) do ModelClass end let(:type_field) do 'bar' end let(:index_field) do 'foo' end it_behaves_like 'a search request that can be paginated' end context 'when the model is a multimodel' do let(:model) do Elasticsearch::Model::Multimodel.new(ModelClass) end let(:type_field) do ['bar'] end let(:index_field) do ['foo'] end it_behaves_like 'a search request that can be paginated' end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/pagination/will_paginate_spec.rb0000644000004100000410000001554214217570270032506 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'Elasticsearch::Model::Response::Response WillPaginate' do before(:all) do class ModelClass def self.index_name; 'foo'; end def self.document_type; 'bar'; end def self.per_page 33 end end # Subclass Response so we can include WillPaginate module without conflicts with Kaminari. class WillPaginateResponse < Elasticsearch::Model::Response::Response include Elasticsearch::Model::Response::Pagination::WillPaginate end end after(:all) do remove_classes(ModelClass, WillPaginateResponse) end let(:response_document) do { 'took' => '5', 'timed_out' => false, '_shards' => {'one' => 'OK'}, 'hits' => { 'total' => 100, 'hits' => (1..100).to_a.map { |i| { _id: i } } } } end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(model, '*') end let(:response) do allow(model).to receive(:client).and_return(client) WillPaginateResponse.new(model, search, response_document).tap do |resp| allow(resp).to receive(:client).and_return(client) end end let(:client) do double('client') end shared_examples_for 'a search request that can be paginated' do describe '#offset' do context 'when per_page and page are set' do before do response.per_page(3).page(3) end it 'sets the correct offset' do expect(response.offset).to eq(6) end end end describe '#length' do context 'when per_page and page are set' do before do response.per_page(3).page(3) end it 'sets the correct offset' do expect(response.length).to eq(3) end end end describe '#paginate' do context 'when there are no settings' do context 'when page is set to nil' do before do response.paginate(page: nil) end it 'uses the defaults' do expect(response.search.definition[:size]).to eq(default_per_page) expect(response.search.definition[:from]).to eq(0) end end context 'when page is set to a value' do before do response.paginate(page: 2) end it 'uses the defaults' do expect(response.search.definition[:size]).to eq(default_per_page) expect(response.search.definition[:from]).to eq(default_per_page) end end context 'when a custom page and per_page is set' do before do response.paginate(page: 3, per_page: 9) end it 'uses the custom values' do expect(response.search.definition[:size]).to eq(9) expect(response.search.definition[:from]).to eq(18) end end context 'fall back to first page if invalid value is provided' do before do response.paginate(page: -1) end it 'uses the custom values' do expect(response.search.definition[:size]).to eq(default_per_page) expect(response.search.definition[:from]).to eq(0) end end end end describe '#page' do context 'when a value is provided for page' do before do response.page(5) end it 'calculates the correct :size and :from' do expect(response.search.definition[:size]).to eq(default_per_page) expect(response.search.definition[:from]).to eq(default_per_page * 4) end end context 'when a value is provided for page and per_page' do before do response.page(5).per_page(3) end it 'calculates the correct :size and :from' do expect(response.search.definition[:size]).to eq(3) expect(response.search.definition[:from]).to eq(12) end end context 'when a value is provided for per_page and page' do before do response.per_page(3).page(5) end it 'calculates the correct :size and :from' do expect(response.search.definition[:size]).to eq(3) expect(response.search.definition[:from]).to eq(12) end end end describe '#current_page' do context 'when no values are set' do before do response.paginate({}) end it 'returns the first page' do expect(response.current_page).to eq(1) end end context 'when values are provided for per_page and page' do before do response.paginate(page: 3, per_page: 9) end it 'calculates the correct current page' do expect(response.current_page).to eq(3) end end context 'when #paginate has not been called on the response' do it 'returns nil' do expect(response.current_page).to be_nil end end end describe '#per_page' do context 'when a value is set via the #paginate method' do before do response.paginate(per_page: 8) end it 'returns the per_page value' do expect(response.per_page).to eq(8) end end context 'when a value is set via the #per_page method' do before do response.per_page(8) end it 'returns the per_page value' do expect(response.per_page).to eq(8) end end end describe '#total_entries' do before do allow(response).to receive(:results).and_return(double('results', total: 100)) end it 'returns the total results' do expect(response.total_entries).to eq(100) end end end context 'when the model is a single one' do let(:model) do ModelClass end let(:default_per_page) do 33 end it_behaves_like 'a search request that can be paginated' end context 'when the model is a multimodel' do let(:model) do Elasticsearch::Model::Multimodel.new(ModelClass) end let(:default_per_page) do ::WillPaginate.per_page end it_behaves_like 'a search request that can be paginated' end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/records_spec.rb0000644000004100000410000000670714217570270027202 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Response::Records do before(:all) do class DummyCollection include Enumerable def each(&block); ['FOO'].each(&block); end def size; ['FOO'].size; end def empty?; ['FOO'].empty?; end def foo; 'BAR'; end end class DummyModel def self.index_name; 'foo'; end def self.document_type; 'bar'; end def self.find(*args) DummyCollection.new end end end after(:all) do remove_classes(DummyCollection, DummyModel) end let(:response_document) do { 'hits' => { 'total' => 123, 'max_score' => 456, 'hits' => [{'_id' => '1', 'foo' => 'bar'}] } } end let(:results) do Elasticsearch::Model::Response::Results.new(DummyModel, response_document) end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(DummyModel, '*').tap do |request| allow(request).to receive(:execute!).and_return(response_document) end end let(:response) do Elasticsearch::Model::Response::Response.new(DummyModel, search) end let(:records) do described_class.new(DummyModel, response) end context 'when the records are accessed' do it 'returns the records' do expect(records.records.size).to eq(1) expect(records.records.first).to eq('FOO') end it 'delegates methods to records' do expect(records.foo).to eq('BAR') end end describe '#each_with_hit' do it 'returns each record with its Elasticsearch hit' do records.each_with_hit do |record, hit| expect(record).to eq('FOO') expect(hit.foo).to eq('bar') end end end describe '#map_with_hit' do let(:value) do records.map_with_hit { |record, hit| "#{record}---#{hit.foo}" } end it 'returns each record with its Elasticsearch hit' do expect(value).to eq(['FOO---bar']) end end describe '#ids' do it 'returns the ids' do expect(records.ids).to eq(['1']) end end context 'when an adapter is used' do before do module DummyAdapter module RecordsMixin def records ['FOOBAR'] end end def records_mixin RecordsMixin end; module_function :records_mixin end allow(Elasticsearch::Model::Adapter).to receive(:from_class).and_return(DummyAdapter) end after do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyAdapter) Object.send(:remove_const, :DummyAdapter) if defined?(DummyAdapter) end it 'delegates the records method to the adapter' do expect(records.records).to eq(['FOOBAR']) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/response_spec.rb0000644000004100000410000000744114217570270027373 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Response::Response do before(:all) do class OriginClass def self.index_name; 'foo'; end def self.document_type; 'bar'; end end end after(:all) do remove_classes(OriginClass) end let(:response_document) do { 'took' => '5', 'timed_out' => false, '_shards' => {'one' => 'OK'}, 'hits' => { 'hits' => [] }, 'aggregations' => {'foo' => {'bar' => 10}}, 'suggest' => {'my_suggest' => [ { 'text' => 'foo', 'options' => [ { 'text' => 'Foo', 'score' => 2.0 }, { 'text' => 'Bar', 'score' => 1.0 } ] } ]}} end let(:search) do Elasticsearch::Model::Searching::SearchRequest.new(OriginClass, '*').tap do |request| allow(request).to receive(:execute!).and_return(response_document) end end let(:response) do Elasticsearch::Model::Response::Response.new(OriginClass, search) end it 'performs the Elasticsearch request lazily' do expect(search).not_to receive(:execute!) response end describe '#klass' do it 'returns the class' do expect(response.klass).to be(OriginClass) end end describe '#search' do it 'returns the search object' do expect(response.search).to eq(search) end end describe '#took' do it 'returns the took field' do expect(response.took).to eq('5') end end describe '#timed_out' do it 'returns the timed_out field' do expect(response.timed_out).to eq(false) end end describe '#shards' do it 'returns a Hashie::Mash' do expect(response.shards.one).to eq('OK') end end describe '#response' do it 'returns the response document' do expect(response.response).to eq(response_document) end end describe '#results' do it 'provides access to the results' do expect(response.results).to be_a(Elasticsearch::Model::Response::Results) expect(response.size).to be(0) end end describe '#records' do it 'provides access to the records' do expect(response.records).to be_a(Elasticsearch::Model::Response::Records) expect(response.size).to be(0) end end describe 'enumerable methods' do it 'delegates the methods to the results' do expect(response.empty?).to be(true) end end describe 'aggregations' do it 'provides access to the aggregations' do expect(response.aggregations).to be_a(Hashie::Mash) expect(response.aggregations.foo.bar).to eq(10) end end describe 'suggestions' do it 'provides access to the suggestions' do expect(response.suggestions).to be_a(Hashie::Mash) expect(response.suggestions.my_suggest.first.options.first.text).to eq('Foo') expect(response.suggestions.terms).to eq([ 'Foo', 'Bar' ]) end context 'when there are no suggestions' do let(:response_document) do { } end it 'returns an empty list' do expect(response.suggestions.terms).to eq([ ]) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/response/result_spec.rb0000644000004100000410000001027414217570270027051 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' require 'active_support/json/encoding' describe Elasticsearch::Model::Response::Result do let(:result) do described_class.new(foo: 'bar', bar: { bam: 'baz' }) end it 'provides access to the properties' do expect(result.foo).to eq('bar') expect(result.bar.bam).to eq('baz') expect { result.xoxo }.to raise_exception(NoMethodError) end describe '#id' do let(:result) do described_class.new(foo: 'bar', _id: 42, _source: { id: 12 }) end it 'returns the _id field' do expect(result.id).to eq(42) end it 'provides access to the source id field' do expect(result._source.id).to eq(12) end end describe '#type' do let(:result) do described_class.new(foo: 'bar', _type: 'baz', _source: { type: 'BAM' }) end it 'returns the _type field' do expect(result.type).to eq('baz') end it 'provides access to the source type field' do expect(result._source.type).to eq('BAM') end end describe 'method delegation' do let(:result) do described_class.new(foo: 'bar', _source: { bar: { bam: 'baz' } }) end it 'provides access to the _source field via a method' do expect(result._source).to eq('bar' => { 'bam' => 'baz' }) end it 'is recognized by #method' do expect(result.method :bar).to be_a Method end it 'respond_to? still works' do expect(result.respond_to? :bar).to be true end context 'when methods map to keys in subdocuments of the response from Elasticsearch' do it 'provides access to top level fields via a method' do expect(result.foo).to eq('bar') expect(result.fetch(:foo)).to eq('bar') expect(result.fetch(:does_not_exist, 'moo')).to eq('moo') end it 'responds to hash methods' do expect(result.keys).to eq(['foo', '_source']) expect(result.to_hash).to eq('foo' => 'bar', '_source' => { 'bar' => { 'bam' => 'baz' } }) end it 'provides access to fields in the _source subdocument via a method' do expect(result.bar).to eq('bam' => 'baz') expect(result.bar.bam).to eq('baz') expect(result._source.bar).to eq('bam' => 'baz') expect(result._source.bar.bam).to eq('baz') end context 'when boolean methods are called' do it 'provides access to top level fields via a method' do expect(result.foo?).to eq(true) expect(result.boo?).to eq(false) end it 'delegates to fields in the _source subdocument via a method' do expect(result.bar?).to eq(true) expect(result.bar.bam?).to eq(true) expect(result.boo?).to eq(false) expect(result.bar.boo?).to eq(false) expect(result._source.bar?).to eq(true) expect(result._source.bar.bam?).to eq(true) expect(result._source.boo?).to eq(false) expect(result._source.bar.boo?).to eq(false) end end end context 'when methods do not map to keys in subdocuments of the response from Elasticsearch' do it 'raises a NoMethodError' do expect { result.does_not_exist }.to raise_exception(NoMethodError) end end end describe '#as_json' do let(:result) do described_class.new(foo: 'bar', _source: { bar: { bam: 'baz' } }) end it 'returns a json string' do expect(result.as_json(except: 'foo')).to eq({'_source'=>{'bar'=>{'bam'=>'baz'}}}) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/naming_spec.rb0000644000004100000410000001207614217570270025150 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe 'naming' do before(:all) do class ::DummyNamingModel extend ActiveModel::Naming extend Elasticsearch::Model::Naming::ClassMethods include Elasticsearch::Model::Naming::InstanceMethods end module ::MyNamespace class DummyNamingModelInNamespace extend ActiveModel::Naming extend Elasticsearch::Model::Naming::ClassMethods include Elasticsearch::Model::Naming::InstanceMethods end end end after(:all) do remove_classes(DummyNamingModel, MyNamespace) end it 'returns the default index name' do expect(DummyNamingModel.index_name).to eq('dummy_naming_models') expect(DummyNamingModel.new.index_name).to eq('dummy_naming_models') end it 'returns the sanitized defualt index name for namespaced models' do expect(::MyNamespace::DummyNamingModelInNamespace.index_name).to eq('my_namespace-dummy_naming_model_in_namespaces') expect(::MyNamespace::DummyNamingModelInNamespace.new.index_name).to eq('my_namespace-dummy_naming_model_in_namespaces') end it 'returns nil' do expect(DummyNamingModel.document_type).to be_nil expect(DummyNamingModel.new.document_type).to be_nil end describe '#index_name' do context 'when the index name is set on the class' do before do DummyNamingModel.index_name 'foobar' end it 'sets the index_name' do expect(DummyNamingModel.index_name).to eq('foobar') end end context 'when the index name is set on an instance' do before do instance.index_name 'foobar_d' end let(:instance) do DummyNamingModel.new end it 'sets the index name on the instance' do expect(instance.index_name).to eq('foobar_d') end context 'when the index name is set with a proc' do before do modifier = 'r' instance.index_name Proc.new{ "foobar_#{modifier}" } end it 'sets the index name on the instance' do expect(instance.index_name).to eq('foobar_r') end end end end describe '#index_name=' do before do DummyNamingModel.index_name = 'foobar_index_S' end it 'changes the index name' do expect(DummyNamingModel.index_name).to eq('foobar_index_S') end context 'when the method is called on an instance' do let(:instance) do DummyNamingModel.new end before do instance.index_name = 'foobar_index_s' end it 'changes the index name' do expect(instance.index_name).to eq('foobar_index_s') end it 'does not change the index name on the class' do expect(DummyNamingModel.index_name).to eq('foobar_index_S') end end context 'when the index name is changed with a proc' do before do modifier2 = 'y' DummyNamingModel.index_name = Proc.new{ "foobar_index_#{modifier2}" } end it 'changes the index name' do expect(DummyNamingModel.index_name).to eq('foobar_index_y') end end end describe '#document_type' do it 'returns nil' do expect(DummyNamingModel.document_type).to be_nil end context 'when the method is called with an argument' do before do DummyNamingModel.document_type 'foo' end it 'changes the document type' do expect(DummyNamingModel.document_type).to eq('foo') end end context 'when the method is called on an instance' do let(:instance) do DummyNamingModel.new end before do instance.document_type 'foobar_d' end it 'changes the document type' do expect(instance.document_type).to eq('foobar_d') end end end describe '#document_type=' do context 'when the method is called on the class' do before do DummyNamingModel.document_type = 'foo_z' end it 'changes the document type' do expect(DummyNamingModel.document_type).to eq('foo_z') end end context 'when the method is called on an instance' do let(:instance) do DummyNamingModel.new end before do instance.document_type = 'foobar_b' end it 'changes the document type' do expect(instance.document_type).to eq('foobar_b') end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/serializing_spec.rb0000644000004100000410000000224214217570270026211 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Serializing do before(:all) do class DummyClass include Elasticsearch::Model::Serializing::InstanceMethods def as_json(options={}) 'HASH' end end end after(:all) do remove_classes(DummyClass) end it 'delegates to #as_json by default' do expect(DummyClass.new.as_indexed_json).to eq('HASH') end end elasticsearch-model-7.2.1/spec/elasticsearch/model/hash_wrapper_spec.rb0000644000004100000410000000207714217570270026362 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::HashWrapper, if: Hashie::VERSION >= '3.5.3' do before do expect(Hashie.logger).to receive(:warn).never end it 'does not print a warning for re-defined methods' do Elasticsearch::Model::HashWrapper.new(:foo => 'bar', :sort => true) end end elasticsearch-model-7.2.1/spec/elasticsearch/model/indexing_spec.rb0000644000004100000410000007367714217570270025522 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Indexing do before(:all) do class ::DummyIndexingModel extend ActiveModel::Naming extend Elasticsearch::Model::Naming::ClassMethods extend Elasticsearch::Model::Indexing::ClassMethods def self.foo 'bar' end end class NotFound < Exception; end end after(:all) do remove_classes(DummyIndexingModel, NotFound) end describe 'the Settings class' do it 'should be convertible to a hash' do expect(Elasticsearch::Model::Indexing::Settings.new(foo: 'bar').to_hash).to eq(foo: 'bar') end it 'should be convertible to json' do expect(Elasticsearch::Model::Indexing::Settings.new(foo: 'bar').as_json).to eq(foo: 'bar') end end describe '#settings' do it 'returns an instance of the Settings class' do expect(DummyIndexingModel.settings).to be_a(Elasticsearch::Model::Indexing::Settings) end context 'when the settings are updated' do before do DummyIndexingModel.settings(foo: 'boo') DummyIndexingModel.settings(bar: 'bam') end it 'updates the settings on the class' do expect(DummyIndexingModel.settings.to_hash).to eq(foo: 'boo', bar: 'bam') end end context 'when the settings are updated with a yml file' do before do DummyIndexingModel.settings File.open('spec/support/model.yml') DummyIndexingModel.settings bar: 'bam' end it 'updates the settings on the class' do expect(DummyIndexingModel.settings.to_hash).to eq(foo: 'boo', bar: 'bam', 'baz' => 'qux') end end context 'when the settings are updated with a json file' do before do DummyIndexingModel.settings File.open('spec/support/model.json') DummyIndexingModel.settings bar: 'bam' end it 'updates the settings on the class' do expect(DummyIndexingModel.settings.to_hash).to eq(foo: 'boo', bar: 'bam', 'baz' => 'qux', 'laz' => 'qux') end end end describe '#mappings' do let(:expected_mapping_hash) do { :mytype => { foo: 'bar', :properties => {} } } end it 'returns an instance of the Mappings class' do expect(DummyIndexingModel.mappings).to be_a(Elasticsearch::Model::Indexing::Mappings) end it 'does not raise an exception when there is no type passed to the #initialize method' do expect(Elasticsearch::Model::Indexing::Mappings.new) end it 'should be convertible to a hash' do expect(Elasticsearch::Model::Indexing::Mappings.new(:mytype, { foo: 'bar' }).to_hash).to eq(expected_mapping_hash) end it 'should be convertible to json' do expect(Elasticsearch::Model::Indexing::Mappings.new(:mytype, { foo: 'bar' }).as_json).to eq(expected_mapping_hash) end context 'when a type is specified' do let(:mappings) do Elasticsearch::Model::Indexing::Mappings.new(:mytype) end before do mappings.indexes :foo, { type: 'boolean', include_in_all: false } mappings.indexes :bar end it 'creates the correct mapping definition' do expect(mappings.to_hash[:mytype][:properties][:foo][:type]).to eq('boolean') end it 'uses text as the default field type' do expect(mappings.to_hash[:mytype][:properties][:bar][:type]).to eq('text') end context 'when the \'include_type_name\' option is specified' do let(:mappings) do Elasticsearch::Model::Indexing::Mappings.new(:mytype, include_type_name: true) end before do mappings.indexes :foo, { type: 'boolean', include_in_all: false } end it 'creates the correct mapping definition' do expect(mappings.to_hash[:mytype][:properties][:foo][:type]).to eq('boolean') end it 'sets the \'include_type_name\' option' do expect(mappings.to_hash[:mytype][:include_type_name]).to eq(true) end end end context 'when a type is not specified' do let(:mappings) do Elasticsearch::Model::Indexing::Mappings.new end before do mappings.indexes :foo, { type: 'boolean', include_in_all: false } mappings.indexes :bar end it 'creates the correct mapping definition' do expect(mappings.to_hash[:properties][:foo][:type]).to eq('boolean') end it 'uses text as the default type' do expect(mappings.to_hash[:properties][:bar][:type]).to eq('text') end end context 'when specific mappings are defined' do let(:mappings) do Elasticsearch::Model::Indexing::Mappings.new(:mytype, include_type_name: true) end before do mappings.indexes :foo, { type: 'boolean', include_in_all: false } mappings.indexes :bar end it 'creates the correct mapping definition' do expect(mappings.to_hash[:mytype][:properties][:foo][:type]).to eq('boolean') end it 'uses text as the default type' do expect(mappings.to_hash[:mytype][:properties][:bar][:type]).to eq('text') end context 'when mappings are defined for multiple fields' do before do mappings.indexes :my_field, type: 'text' do indexes :raw, type: 'keyword' end end it 'defines the mapping for all the fields' do expect(mappings.to_hash[:mytype][:properties][:my_field][:type]).to eq('text') expect(mappings.to_hash[:mytype][:properties][:my_field][:fields][:raw][:type]).to eq('keyword') expect(mappings.to_hash[:mytype][:properties][:my_field][:fields][:raw][:properties]).to be_nil end end context 'when embedded properties are defined' do before do mappings.indexes :foo do indexes :bar end mappings.indexes :foo_object, type: 'object' do indexes :bar end mappings.indexes :foo_nested, type: 'nested' do indexes :bar end mappings.indexes :foo_nested_as_symbol, type: :nested do indexes :bar end end it 'defines mappings for the embedded properties' do expect(mappings.to_hash[:mytype][:properties][:foo][:type]).to eq('object') expect(mappings.to_hash[:mytype][:properties][:foo][:properties][:bar][:type]).to eq('text') expect(mappings.to_hash[:mytype][:properties][:foo][:fields]).to be_nil expect(mappings.to_hash[:mytype][:properties][:foo_object][:type]).to eq('object') expect(mappings.to_hash[:mytype][:properties][:foo_object][:properties][:bar][:type]).to eq('text') expect(mappings.to_hash[:mytype][:properties][:foo_object][:fields]).to be_nil expect(mappings.to_hash[:mytype][:properties][:foo_nested][:type]).to eq('nested') expect(mappings.to_hash[:mytype][:properties][:foo_nested][:properties][:bar][:type]).to eq('text') expect(mappings.to_hash[:mytype][:properties][:foo_nested][:fields]).to be_nil expect(mappings.to_hash[:mytype][:properties][:foo_nested_as_symbol][:type]).to eq(:nested) expect(mappings.to_hash[:mytype][:properties][:foo_nested_as_symbol][:properties]).not_to be_nil expect(mappings.to_hash[:mytype][:properties][:foo_nested_as_symbol][:fields]).to be_nil end it 'defines the settings' do expect(mappings.to_hash[:mytype][:include_type_name]).to be(true) end end end context 'when the method is called on a class' do before do DummyIndexingModel.mappings(foo: 'boo') DummyIndexingModel.mappings(bar: 'bam') end let(:expected_mappings_hash) do { foo: "boo", bar: "bam", properties: {} } end it 'sets the mappings' do expect(DummyIndexingModel.mappings.to_hash).to eq(expected_mappings_hash) end context 'when the method is called with a block' do before do DummyIndexingModel.mapping do indexes :foo, type: 'boolean' end end it 'sets the mappings' do expect(DummyIndexingModel.mapping.to_hash[:properties][:foo][:type]).to eq('boolean') end end context 'when the class has a document_type' do before do DummyIndexingModel.instance_variable_set(:@mapping, nil) DummyIndexingModel.document_type(:mytype) DummyIndexingModel.mappings(foo: 'boo') DummyIndexingModel.mappings(bar: 'bam') end let(:expected_mappings_hash) do { mytype: { foo: "boo", bar: "bam", properties: {} } } end it 'sets the mappings' do expect(DummyIndexingModel.mappings.to_hash).to eq(expected_mappings_hash) end end end end describe 'instance methods' do before(:all) do class ::DummyIndexingModelWithCallbacks extend Elasticsearch::Model::Indexing::ClassMethods include Elasticsearch::Model::Indexing::InstanceMethods def self.before_save(&block) (@callbacks ||= {})[block.hash] = block end def changes_to_save {:foo => ['One', 'Two']} end end class ::DummyIndexingModelWithNoChanges extend Elasticsearch::Model::Indexing::ClassMethods include Elasticsearch::Model::Indexing::InstanceMethods def self.before_save(&block) (@callbacks ||= {})[block.hash] = block end def changes_to_save {} end end class ::DummyIndexingModelWithCallbacksAndCustomAsIndexedJson extend Elasticsearch::Model::Indexing::ClassMethods include Elasticsearch::Model::Indexing::InstanceMethods def self.before_save(&block) (@callbacks ||= {})[block.hash] = block end def changes_to_save {:foo => ['A', 'B'], :bar => ['C', 'D']} end def as_indexed_json(options={}) { :foo => 'B' } end end class ::DummyIndexingModelWithOldDirty extend Elasticsearch::Model::Indexing::ClassMethods include Elasticsearch::Model::Indexing::InstanceMethods def self.before_save(&block) (@callbacks ||= {})[block.hash] = block end def changes {:foo => ['One', 'Two']} end end end after(:all) do Object.send(:remove_const, :DummyIndexingModelWithCallbacks) if defined?(DummyIndexingModelWithCallbacks) Object.send(:remove_const, :DummyIndexingModelWithNoChanges) if defined?(DummyIndexingModelWithNoChanges) Object.send(:remove_const, :DummyIndexingModelWithCallbacksAndCustomAsIndexedJson) if defined?(DummyIndexingModelWithCallbacksAndCustomAsIndexedJson) Object.send(:remove_const, :DummyIndexingModelWithOldDirty) if defined?(DummyIndexingModelWithOldDirty) end context 'when the module is included' do context 'when the model uses the old ActiveModel::Dirty' do before do DummyIndexingModelWithOldDirty.__send__ :include, Elasticsearch::Model::Indexing::InstanceMethods end it 'registers callbacks' do expect(DummyIndexingModelWithOldDirty.instance_variable_get(:@callbacks)).not_to be_empty end let(:instance) do DummyIndexingModelWithOldDirty.new end it 'sets the @__changed_model_attributes variable before the callback' do DummyIndexingModelWithOldDirty.instance_variable_get(:@callbacks).each do |n, callback| instance.instance_eval(&callback) expect(instance.instance_variable_get(:@__changed_model_attributes)).to eq(foo: 'Two') end end end context 'when the model users the current ActiveModel::Dirty' do before do DummyIndexingModelWithCallbacks.__send__ :include, Elasticsearch::Model::Indexing::InstanceMethods end it 'registers callbacks' do expect(DummyIndexingModelWithCallbacks.instance_variable_get(:@callbacks)).not_to be_empty end let(:instance) do DummyIndexingModelWithCallbacks.new end it 'sets the @__changed_model_attributes variable before the callback' do DummyIndexingModelWithCallbacks.instance_variable_get(:@callbacks).each do |n, callback| instance.instance_eval(&callback) expect(instance.instance_variable_get(:@__changed_model_attributes)).to eq(foo: 'Two') end end end end describe '#index_document' do before do expect(instance).to receive(:client).and_return(client) expect(instance).to receive(:as_indexed_json).and_return('JSON') expect(instance).to receive(:index_name).and_return('foo') expect(instance).to receive(:document_type).twice.and_return('bar') expect(instance).to receive(:id).and_return('1') end let(:client) do double('client') end let(:instance) do DummyIndexingModelWithCallbacks.new end context 'when no options are passed to the method' do before do expect(client).to receive(:index).with(index: 'foo', type: 'bar', id: '1', body: 'JSON').and_return(true) end it 'provides the method on an instance' do expect(instance.index_document).to be(true) end end context 'when extra options are passed to the method' do before do expect(client).to receive(:index).with(index: 'foo', type: 'bar', id: '1', body: 'JSON', parent: 'A').and_return(true) end it 'passes the extra options to the method call on the client' do expect(instance.index_document(parent: 'A')).to be(true) end end end describe '#delete_document' do before do expect(instance).to receive(:client).and_return(client) expect(instance).to receive(:index_name).and_return('foo') expect(instance).to receive(:document_type).twice.and_return('bar') expect(instance).to receive(:id).and_return('1') end let(:client) do double('client') end let(:instance) do DummyIndexingModelWithCallbacks.new end context 'when no options are passed to the method' do before do expect(client).to receive(:delete).with(index: 'foo', type: 'bar', id: '1').and_return(true) end it 'provides the method on an instance' do expect(instance.delete_document).to be(true) end end context 'when extra options are passed to the method' do before do expect(client).to receive(:delete).with(index: 'foo', type: 'bar', id: '1', parent: 'A').and_return(true) end it 'passes the extra options to the method call on the client' do expect(instance.delete_document(parent: 'A')).to be(true) end end end describe '#update_document' do let(:client) do double('client') end let(:instance) do DummyIndexingModelWithCallbacks.new end context 'when no changes are present' do before do expect(instance).to receive(:index_document).and_return(true) expect(client).to receive(:update).never instance.instance_variable_set(:@__changed_model_attributes, nil) end it 'updates the document' do expect(instance.update_document).to be(true) end end context 'when changes are present' do before do allow(instance).to receive(:client).and_return(client) allow(instance).to receive(:index_name).and_return('foo') allow(instance).to receive(:document_type).and_return('bar') allow(instance).to receive(:id).and_return('1') end context 'when the changes are included in the as_indexed_json representation' do before do instance.instance_variable_set(:@__changed_model_attributes, { foo: 'bar' }) expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { foo: 'bar' } }).and_return(true) end it 'updates the document' do expect(instance.update_document).to be(true) end end context 'when the changes are not all included in the as_indexed_json representation' do let(:instance) do DummyIndexingModelWithCallbacksAndCustomAsIndexedJson.new end before do instance.instance_variable_set(:@__changed_model_attributes, {'foo' => 'B', 'bar' => 'D' }) expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { foo: 'B' } }).and_return(true) end it 'updates the document' do expect(instance.update_document).to be(true) end end context 'when none of the changes are included in the as_indexed_json representation' do let(:instance) do DummyIndexingModelWithCallbacksAndCustomAsIndexedJson.new end before do instance.instance_variable_set(:@__changed_model_attributes, {'bar' => 'D' }) end it 'does not update the document' do expect(instance.update_document).to_not be(true) end end context 'when there are partial updates' do let(:instance) do DummyIndexingModelWithCallbacksAndCustomAsIndexedJson.new end before do instance.instance_variable_set(:@__changed_model_attributes, { 'foo' => { 'bar' => 'BAR'} }) expect(instance).to receive(:as_indexed_json).and_return('foo' => 'BAR') expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { 'foo' => 'BAR' } }).and_return(true) end it 'updates the document' do expect(instance.update_document).to be(true) end end end end describe '#update_document_attributes' do let(:client) do double('client') end let(:instance) do DummyIndexingModelWithCallbacks.new end context 'when changes are present' do before do expect(instance).to receive(:client).and_return(client) expect(instance).to receive(:index_name).and_return('foo') expect(instance).to receive(:document_type).twice.and_return('bar') expect(instance).to receive(:id).and_return('1') instance.instance_variable_set(:@__changed_model_attributes, { author: 'john' }) end context 'when no options are specified' do before do expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { title: 'green' } }).and_return(true) end it 'updates the document' do expect(instance.update_document_attributes(title: 'green')).to be(true) end end context 'when extra options are provided' do before do expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { title: 'green' } }, refresh: true).and_return(true) end it 'updates the document' do expect(instance.update_document_attributes({ title: 'green' }, refresh: true)).to be(true) end end end end end describe '#index_exists?' do before do expect(DummyIndexingModel).to receive(:client).and_return(client) end context 'when the index exists' do let(:client) do double('client', indices: double('indices', exists: true)) end it 'returns true' do expect(DummyIndexingModel.index_exists?).to be(true) end end context 'when the index does not exists' do let(:client) do double('client', indices: double('indices', exists: false)) end it 'returns false' do expect(DummyIndexingModel.index_exists?).to be(false) end end end describe '#delete_index!' do before(:all) do class ::DummyIndexingModelForRecreate extend ActiveModel::Naming extend Elasticsearch::Model::Naming::ClassMethods extend Elasticsearch::Model::Indexing::ClassMethods end end after(:all) do Object.send(:remove_const, :DummyIndexingModelForRecreate) if defined?(DummyIndexingModelForRecreate) end context 'when the index is not found' do let(:logger) { nil } let(:transport) do Elasticsearch::Transport::Client.new(logger: logger) end let(:client) do double('client', indices: indices, transport: transport) end let(:indices) do double('indices').tap do |ind| expect(ind).to receive(:delete).and_raise(NotFound) end end before do expect(DummyIndexingModelForRecreate).to receive(:client).at_most(3).times.and_return(client) end context 'when the force option is true' do it 'deletes the index without raising an exception' do expect(DummyIndexingModelForRecreate.delete_index!(force: true)).to be_nil end context 'when the client has a logger' do let(:logger) do Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG } end let(:client) do double('client', indices: indices, transport: transport) end it 'deletes the index without raising an exception' do expect(DummyIndexingModelForRecreate.delete_index!(force: true)).to be_nil end it 'logs the message that the index is not found' do expect(logger).to receive(:debug) expect(DummyIndexingModelForRecreate.delete_index!(force: true)).to be_nil end end end context 'when the force option is not provided' do it 'raises an exception' do expect { DummyIndexingModelForRecreate.delete_index! }.to raise_exception(NotFound) end end context 'when the exception is not NotFound' do let(:indices) do double('indices').tap do |ind| expect(ind).to receive(:delete).and_raise(Exception) end end it 'raises an exception' do expect { DummyIndexingModelForRecreate.delete_index! }.to raise_exception(Exception) end end end context 'when an index name is provided in the options' do before do expect(DummyIndexingModelForRecreate).to receive(:client).and_return(client) expect(indices).to receive(:delete).with(index: 'custom-foo') end let(:client) do double('client', indices: indices) end let(:indices) do double('indices', delete: true) end it 'uses the index name' do expect(DummyIndexingModelForRecreate.delete_index!(index: 'custom-foo')) end end end describe '#create_index' do before(:all) do class ::DummyIndexingModelForCreate extend ActiveModel::Naming extend Elasticsearch::Model::Naming::ClassMethods extend Elasticsearch::Model::Indexing::ClassMethods index_name 'foo' settings index: { number_of_shards: 1 } do mappings do indexes :foo, analyzer: 'keyword' end end end end after(:all) do Object.send(:remove_const, :DummyIndexingModelForCreate) if defined?(DummyIndexingModelForCreate) end let(:client) do double('client', indices: indices) end let(:indices) do double('indices') end context 'when the index does not exist' do before do expect(DummyIndexingModelForCreate).to receive(:client).and_return(client) expect(DummyIndexingModelForCreate).to receive(:index_exists?).and_return(false) end context 'when options are not provided' do let(:expected_body) do { mappings: { properties: { foo: { analyzer: 'keyword', type: 'text' } } }, settings: { index: { number_of_shards: 1 } } } end before do expect(indices).to receive(:create).with(index: 'foo', body: expected_body).and_return(true) end it 'creates the index' do expect(DummyIndexingModelForCreate.create_index!).to be(true) end end context 'when options are provided' do let(:expected_body) do { mappings: { foobar: { properties: { foo: { analyzer: 'bar' } } } }, settings: { index: { number_of_shards: 3 } } } end before do expect(indices).to receive(:create).with(index: 'foobar', body: expected_body).and_return(true) end it 'creates the index' do expect(DummyIndexingModelForCreate.create_index! \ index: 'foobar', settings: { index: { number_of_shards: 3 } }, mappings: { foobar: { properties: { foo: { analyzer: 'bar' } } } } ).to be(true) end end end context 'when the index exists' do before do expect(DummyIndexingModelForCreate).to receive(:index_exists?).and_return(true) expect(indices).to receive(:create).never end it 'does not create the index' do expect(DummyIndexingModelForCreate.create_index!).to be_nil end end context 'when creating the index raises an exception' do before do expect(DummyIndexingModelForCreate).to receive(:client).and_return(client) expect(DummyIndexingModelForCreate).to receive(:delete_index!).and_return(true) expect(DummyIndexingModelForCreate).to receive(:index_exists?).and_return(false) expect(indices).to receive(:create).and_raise(Exception) end it 'raises the exception' do expect { DummyIndexingModelForCreate.create_index!(force: true) }.to raise_exception(Exception) end end context 'when an index name is provided in the options' do before do expect(DummyIndexingModelForCreate).to receive(:client).and_return(client).twice expect(indices).to receive(:exists).and_return(false) expect(indices).to receive(:create).with(index: 'custom-foo', body: expected_body) end let(:expected_body) do { mappings: { properties: { foo: { analyzer: 'keyword', type: 'text' } } }, settings: { index: { number_of_shards: 1 } } } end it 'uses the index name' do expect(DummyIndexingModelForCreate.create_index!(index: 'custom-foo')) end end context 'when the logging level is debug' end describe '#refresh_index!' do before(:all) do class ::DummyIndexingModelForRefresh extend ActiveModel::Naming extend Elasticsearch::Model::Naming::ClassMethods extend Elasticsearch::Model::Indexing::ClassMethods index_name 'foo' settings index: { number_of_shards: 1 } do mappings do indexes :foo, analyzer: 'keyword' end end end end after(:all) do Object.send(:remove_const, :DummyIndexingModelForRefresh) if defined?(DummyIndexingModelForRefresh) end let(:client) do double('client', indices: indices, transport: transport) end let(:transport) do Elasticsearch::Transport::Client.new(logger: nil) end let(:indices) do double('indices') end before do expect(DummyIndexingModelForRefresh).to receive(:client).at_most(3).times.and_return(client) end context 'when the force option is true' do context 'when the operation raises a NotFound exception' do before do expect(indices).to receive(:refresh).and_raise(NotFound) end it 'does not raise an exception' do expect(DummyIndexingModelForRefresh.refresh_index!(force: true)).to be_nil end context 'when the client has a logger' do let(:logger) do Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG } end let(:client) do double('client', indices: indices, transport: transport) end let(:transport) do Elasticsearch::Transport::Client.new(logger: logger) end it 'does not raise an exception' do expect(DummyIndexingModelForRefresh.refresh_index!(force: true)).to be_nil end it 'logs the message that the index is not found' do expect(logger).to receive(:debug) expect(DummyIndexingModelForRefresh.refresh_index!(force: true)).to be_nil end end end context 'when the operation raises another type of exception' do before do expect(indices).to receive(:refresh).and_raise(Exception) end it 'does not raise an exception' do expect { DummyIndexingModelForRefresh.refresh_index!(force: true) }.to raise_exception(Exception) end end end context 'when an index name is provided in the options' do before do expect(indices).to receive(:refresh).with(index: 'custom-foo') end it 'uses the index name' do expect(DummyIndexingModelForRefresh.refresh_index!(index: 'custom-foo')) end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/proxy_spec.rb0000644000004100000410000000724614217570270025063 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Proxy do before(:all) do class ::DummyProxyModel include Elasticsearch::Model::Proxy def self.foo 'classy foo' end def bar 'insta barr' end def keyword_method(foo: 'default value') foo end def as_json(options) {foo: 'bar'} end end class ::DummyProxyModelWithCallbacks def self.before_save(&block) (@callbacks ||= {})[block.hash] = block end def changes_to_save {:foo => ['One', 'Two']} end end DummyProxyModelWithCallbacks.__send__ :include, Elasticsearch::Model::Proxy end after(:all) do remove_classes(DummyProxyModel, DummyProxyModelWithCallbacks) end it 'sets up a proxy method on the class' do expect(DummyProxyModel).to respond_to(:__elasticsearch__) end it 'sets up a proxy method on instances' do expect(DummyProxyModel.new).to respond_to(:__elasticsearch__) end it 'sets up hooks for before_save callbacks' do expect(DummyProxyModelWithCallbacks).to respond_to(:before_save) end it 'delegates methods to the target' do expect(DummyProxyModel.__elasticsearch__).to respond_to(:foo) expect(DummyProxyModel.__elasticsearch__.foo).to eq('classy foo') expect(DummyProxyModel.new.__elasticsearch__).to respond_to(:bar) expect(DummyProxyModel.new.__elasticsearch__.bar).to eq('insta barr') expect { DummyProxyModel.__elasticsearch__.xoxo }.to raise_exception(NoMethodError) expect { DummyProxyModel.new.__elasticsearch__.xoxo }.to raise_exception(NoMethodError) end it 'returns the proxy class from an instance proxy' do expect(DummyProxyModel.new.__elasticsearch__.class.class).to eq(Elasticsearch::Model::Proxy::ClassMethodsProxy) end it 'returns the origin class from an instance proxy' do expect(DummyProxyModel.new.__elasticsearch__.klass).to eq(DummyProxyModel) end it 'delegates #as_json from the proxy to the target' do expect(DummyProxyModel.new.__elasticsearch__.as_json).to eq(foo: 'bar') end it 'includes the proxy in the inspect string' do expect(DummyProxyModel.__elasticsearch__.inspect).to match(/PROXY/) expect(DummyProxyModel.new.__elasticsearch__.inspect).to match(/PROXY/) end context 'when instances are cloned' do let!(:model) do DummyProxyModel.new end let!(:model_target) do model.__elasticsearch__.target end let!(:duplicate) do model.dup end let!(:duplicate_target) do duplicate.__elasticsearch__.target end it 'resets the proxy target' do expect(model).not_to eq(duplicate) expect(model).to eq(model_target) expect(duplicate).to eq(duplicate_target) end end it 'forwards keyword arguments to target methods' do expect(DummyProxyModel.new.__elasticsearch__.keyword_method(foo: 'bar')).to eq('bar') end end elasticsearch-model-7.2.1/spec/elasticsearch/model/client_spec.rb0000644000004100000410000000454014217570270025152 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Client do before(:all) do class ::DummyClientModel extend Elasticsearch::Model::Client::ClassMethods include Elasticsearch::Model::Client::InstanceMethods end end after(:all) do remove_classes(DummyClientModel) end context 'when a class includes the client module class methods' do it 'defines the client module class methods on the model' do expect(DummyClientModel.client).to be_a(Elasticsearch::Client) end end context 'when a class includes the client module instance methods' do it 'defines the client module class methods on the model' do expect(DummyClientModel.new.client).to be_a(Elasticsearch::Client) end end context 'when the client is set on the class' do around do |example| original_client = DummyClientModel.client DummyClientModel.client = 'foobar' example.run DummyClientModel.client = original_client end it 'sets the client on the class' do expect(DummyClientModel.client).to eq('foobar') end it 'sets the client on an instance' do expect(DummyClientModel.new.client).to eq('foobar') end end context 'when the client is set on an instance' do before do model_instance.client = 'foo' end let(:model_instance) do DummyClientModel.new end it 'sets the client on an instance' do expect(model_instance.client).to eq('foo') end it 'does not set the client on the class' do expect(DummyClientModel.client).to be_a(Elasticsearch::Client) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/module_spec.rb0000644000004100000410000000543614217570270025166 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model do describe '#client' do it 'should have a default' do expect(Elasticsearch::Model.client).to be_a(Elasticsearch::Client) end end describe '#client=' do before do Elasticsearch::Model.client = 'Foobar' end it 'should allow the client to be set' do expect(Elasticsearch::Model.client).to eq('Foobar') end end describe 'mixin' do before(:all) do class ::DummyIncludingModel; end class ::DummyIncludingModelWithSearchMethodDefined def self.search(query, options={}) "SEARCH" end end DummyIncludingModel.__send__ :include, Elasticsearch::Model DummyIncludingModelWithSearchMethodDefined.__send__ :include, Elasticsearch::Model end after(:all) do remove_classes(DummyIncludingModel, DummyIncludingModelWithSearchMethodDefined) end it 'should include and set up the proxy' do expect(DummyIncludingModel).to respond_to(:__elasticsearch__) expect(DummyIncludingModel.new).to respond_to(:__elasticsearch__) end it 'should delegate methods to the proxy' do expect(DummyIncludingModel).to respond_to(:search) expect(DummyIncludingModel).to respond_to(:mapping) expect(DummyIncludingModel).to respond_to(:settings) expect(DummyIncludingModel).to respond_to(:index_name) expect(DummyIncludingModel).to respond_to(:document_type) expect(DummyIncludingModel).to respond_to(:import) end it 'should not interfere with existing methods' do expect(DummyIncludingModelWithSearchMethodDefined.search('foo')).to eq('SEARCH') end end describe '#settings' do it 'allows access to the settings' do expect(Elasticsearch::Model.settings).to eq({}) end context 'when settings are changed' do before do Elasticsearch::Model.settings[:foo] = 'bar' end it 'persists the changes' do expect(Elasticsearch::Model.settings[:foo]).to eq('bar') end end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/callbacks_spec.rb0000644000004100000410000000274414217570270025617 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Callbacks do before(:all) do class ::DummyCallbacksModel end module DummyCallbacksAdapter module CallbacksMixin end def callbacks_mixin CallbacksMixin end; module_function :callbacks_mixin end end after(:all) do remove_classes(DummyCallbacksModel, DummyCallbacksAdapter) end context 'when a model includes the Callbacks module' do before do Elasticsearch::Model::Callbacks.included(DummyCallbacksModel) end it 'includes the callbacks mixin from the model adapter' do expect(DummyCallbacksModel.ancestors).to include(Elasticsearch::Model::Adapter::Default::Callbacks) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/multimodel_spec.rb0000644000004100000410000000367514217570270026057 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Multimodel do let(:multimodel) do Elasticsearch::Model::Multimodel.new(model_1, model_2) end let(:model_1) do double('Foo', index_name: 'foo_index', document_type: 'foo', to_ary: nil) end let(:model_2) do double('Bar', index_name: 'bar_index', document_type: 'bar', to_ary: nil) end it 'has an index name' do expect(multimodel.index_name).to eq(['foo_index', 'bar_index']) end it 'has an document type' do expect(multimodel.document_type).to eq(['foo', 'bar']) end it 'has a client' do expect(multimodel.client).to eq(Elasticsearch::Model.client) end describe 'the model registry' do before(:all) do class JustAModel include Elasticsearch::Model end class JustAnotherModel include Elasticsearch::Model end end after(:all) do remove_classes(JustAModel, JustAnotherModel) end let(:multimodel) do Elasticsearch::Model::Multimodel.new end it 'includes model in the registry' do expect(multimodel.models).to include(JustAModel) expect(multimodel.models).to include(JustAnotherModel) end end end elasticsearch-model-7.2.1/spec/elasticsearch/model/importing_spec.rb0000644000004100000410000001635614217570270025714 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'spec_helper' describe Elasticsearch::Model::Importing do before(:all) do class DummyImportingModel end module DummyImportingAdapter module ImportingMixin def __find_in_batches(options={}, &block) yield if block_given? end def __transform lambda {|a|} end end def importing_mixin ImportingMixin end; module_function :importing_mixin end end after(:all) do remove_classes(DummyImportingModel, DummyImportingAdapter) end before do allow(Elasticsearch::Model::Adapter).to receive(:from_class).with(DummyImportingModel).and_return(DummyImportingAdapter) DummyImportingModel.__send__ :include, Elasticsearch::Model::Importing end context 'when a model includes the Importing module' do it 'provides importing methods' do expect(DummyImportingModel.respond_to?(:import)).to be(true) expect(DummyImportingModel.respond_to?(:__find_in_batches)).to be(true) end end describe '#import' do before do allow(DummyImportingModel).to receive(:index_name).and_return('foo') allow(DummyImportingModel).to receive(:document_type).and_return('foo') allow(DummyImportingModel).to receive(:index_exists?).and_return(true) allow(DummyImportingModel).to receive(:__batch_to_bulk) allow(client).to receive(:bulk).and_return(response) end let(:client) do double('client') end let(:response) do { 'items' => [] } end context 'when no options are provided' do before do expect(DummyImportingModel).to receive(:client).and_return(client) allow(DummyImportingModel).to receive(:index_exists?).and_return(true) end it 'uses the client to import documents' do expect(DummyImportingModel.import).to eq(0) end end context 'when there is an error' do before do expect(DummyImportingModel).to receive(:client).and_return(client) allow(DummyImportingModel).to receive(:index_exists?).and_return(true) end let(:response) do { 'items' => [{ 'index' => { } }, { 'index' => { 'error' => 'FAILED' } }] } end it 'returns the number of errors' do expect(DummyImportingModel.import).to eq(1) end context 'when the method is called with the option to return the errors' do it 'returns the errors' do expect(DummyImportingModel.import(return: 'errors')).to eq([{ 'index' => { 'error' => 'FAILED' } }]) end end context 'when the method is called with a block' do it 'yields the response to the block' do DummyImportingModel.import do |response| expect(response['items'].size).to eq(2) end end end end context 'when the index does not exist' do before do allow(DummyImportingModel).to receive(:index_exists?).and_return(false) end it 'raises an exception' do expect { DummyImportingModel.import }.to raise_exception(ArgumentError) end end context 'when the method is called with the force option' do before do expect(DummyImportingModel).to receive(:create_index!).with(force: true, index: 'foo').and_return(true) expect(DummyImportingModel).to receive(:__find_in_batches).with(foo: 'bar').and_return(true) end it 'deletes and creates the index' do expect(DummyImportingModel.import(force: true, foo: 'bar')).to eq(0) end end context 'when the method is called with the refresh option' do before do expect(DummyImportingModel).to receive(:refresh_index!).with(index: 'foo').and_return(true) expect(DummyImportingModel).to receive(:__find_in_batches).with(foo: 'bar').and_return(true) end it 'refreshes the index' do expect(DummyImportingModel.import(refresh: true, foo: 'bar')).to eq(0) end end context 'when a different index name is provided' do before do expect(DummyImportingModel).to receive(:client).and_return(client) expect(client).to receive(:bulk).with(body: nil, index: 'my-new-index', type: 'foo').and_return(response) end it 'uses the alternate index name' do expect(DummyImportingModel.import(index: 'my-new-index')).to eq(0) end end context 'when a different document type is provided' do before do expect(DummyImportingModel).to receive(:client).and_return(client) expect(client).to receive(:bulk).with(body: nil, index: 'foo', type: 'my-new-type').and_return(response) end it 'uses the alternate index name' do expect(DummyImportingModel.import(type: 'my-new-type')).to eq(0) end end context 'the transform method' do before do expect(DummyImportingModel).to receive(:client).and_return(client) expect(DummyImportingModel).to receive(:__transform).and_return(transform) expect(DummyImportingModel).to receive(:__batch_to_bulk).with(anything, transform) end let(:transform) do lambda {|a|} end it 'applies the transform method to the results' do expect(DummyImportingModel.import).to eq(0) end end context 'when a transform is provided as an option' do context 'when the transform option is not a lambda' do let(:transform) do 'not_callable' end it 'raises an error' do expect { DummyImportingModel.import(transform: transform) }.to raise_exception(ArgumentError) end end context 'when the transform option is a lambda' do before do expect(DummyImportingModel).to receive(:client).and_return(client) expect(DummyImportingModel).to receive(:__batch_to_bulk).with(anything, transform) end let(:transform) do lambda {|a|} end it 'applies the transform lambda to the results' do expect(DummyImportingModel.import(transform: transform)).to eq(0) end end end context 'when a pipeline is provided as an options' do before do expect(DummyImportingModel).to receive(:client).and_return(client) expect(client).to receive(:bulk).with(body: nil, index: 'foo', type: 'foo', pipeline: 'my-pipeline').and_return(response) end it 'uses the pipeline option' do expect(DummyImportingModel.import(pipeline: 'my-pipeline')).to eq(0) end end end end elasticsearch-model-7.2.1/CHANGELOG.md0000644000004100000410000000706614217570270017310 0ustar www-datawww-data## 0.1.9 * Added a `suggest` method to wrap the suggestions in response * Added the `:includes` option to Adapter::ActiveRecord::Records for eagerly loading associated models * Delegated `max_pages` method properly for Kaminari's `next_page` * Fixed `#dup` behaviour for Elasticsearch::Model * Fixed typos in the README and examples ## 0.1.8 * Added "default per page" methods for pagination with multi model searches * Added a convenience accessor for the `aggregations` part of response * Added a full example with mapping for the completion suggester * Added an integration test for paginating multiple models * Added proper support for the new "multi_fields" in the mapping DSL * Added the `no_timeout` option for `__find_in_batches` in the Mongoid adapter * Added, that index settings can be loaded from any object that responds to `:read` * Added, that index settings/mappings can be loaded from a YAML or JSON file * Added, that String pagination parameters are converted to numbers * Added, that empty block is not required for setting mapping options * Added, that on MyModel#import, an exception is raised if the index does not exists * Changed the Elasticsearch port in the Mongoid example to 9200 * Cleaned up the tests for multiple fields/properties in mapping DSL * Fixed a bug where continuous `#save` calls emptied the `@__changed_attributes` variable * Fixed a buggy test introduced in #335 * Fixed incorrect deserialization of records in the Multiple adapter * Fixed incorrect examples and documentation * Fixed unreliable order of returned results/records in the integration test for the multiple adapter * Fixed, that `param_name` is used when paginating with WillPaginate * Fixed the problem where `document_type` configuration was not propagated to mapping [6 months ago by Miguel Ferna * Refactored the code in `__find_in_batches` to use Enumerable#each_slice * Refactored the string queries in multiple_models_test.rb to avoid quote escaping ## 0.1.7 * Improved examples and instructions in README and code annotations * Prevented index methods to swallow all exceptions * Added the `:validate` option to the `save` method for models * Added support for searching across multiple models (elastic/elasticsearch-rails#345), including documentation, examples and tests ## 0.1.6 * Improved documentation * Added dynamic getter/setter (block/proc) for `MyModel.index_name` * Added the `update_document_attributes` method * Added, that records to import can be limited by the `query` option ## 0.1.5 * Improved documentation * Fixes and improvements to the "will_paginate" integration * Added a `:preprocess` option to the `import` method * Changed, that attributes are fetched from `as_indexed_json` in the `update_document` method * Added an option to the import method to return an array of error messages instead of just count * Fixed many problems with dependency hell * Fixed tests so they run on Ruby 2.2 ## 0.1.2 * Properly delegate existence methods like `result.foo?` to `result._source.foo` * Exception is raised when `type` is not passed to Mappings#new * Allow passing an ActiveRecord scope to the `import` method * Added, that `each_with_hit` and `map_with_hit` in `Elasticsearch::Model::Response::Records` call `to_a` * Added support for [`will_paginate`](https://github.com/mislav/will_paginate) pagination library * Added the ability to transform models during indexing * Added explicit `type` and `id` methods to Response::Result, aliasing `_type` and `_id` ## 0.1.1 * Improved documentation and tests * Fixed Kaminari implementation bugs and inconsistencies ## 0.1.0 (Initial Version) elasticsearch-model-7.2.1/.gitignore0000644000004100000410000000025514217570270017460 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp gemfiles/*.lock elasticsearch-model-7.2.1/examples/0000755000004100000410000000000014217570270017304 5ustar www-datawww-dataelasticsearch-model-7.2.1/examples/couchbase_article.rb0000644000004100000410000000521614217570270023274 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Couchbase and Elasticsearch # =========================== # # https://github.com/couchbase/couchbase-ruby-model $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__) require 'logger' require 'couchbase/model' require 'elasticsearch/model' # Documents are stored as JSON objects in Riak but have rich # semantics, including validations and associations. class Article < Couchbase::Model attribute :title attribute :published_at # view :all, :limit => 10, :descending => true # TODO: Implement view a la # bucket.save_design_doc <<-JSON # { # "_id": "_design/article", # "language": "javascript", # "views": { # "all": { # "map": "function(doc, meta) { emit(doc.id, doc.title); }" # } # } # } # JSON end # Extend the model with Elasticsearch support # Article.__send__ :extend, Elasticsearch::Model::Client::ClassMethods Article.__send__ :extend, Elasticsearch::Model::Searching::ClassMethods Article.__send__ :extend, Elasticsearch::Model::Naming::ClassMethods # Create documents in Riak # Article.create id: '1', title: 'Foo' rescue nil Article.create id: '2', title: 'Bar' rescue nil Article.create id: '3', title: 'Foo Foo' rescue nil # Index data into Elasticsearch # client = Elasticsearch::Client.new log:true client.indices.delete index: 'articles' rescue nil client.bulk index: 'articles', type: 'article', body: Article.find(['1', '2', '3']).map { |a| { index: { _id: a.id, data: a.attributes } } }, refresh: true response = Article.search 'foo', index: 'articles', type: 'article'; Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.to_a'), quiet: true) elasticsearch-model-7.2.1/examples/mongoid_article.rb0000644000004100000410000000507014217570270022772 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Mongoid and Elasticsearch # ========================= # # http://mongoid.org/en/mongoid/index.html $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__) require 'benchmark' require 'logger' require 'ansi/core' require 'mongoid' require 'elasticsearch/model' require 'elasticsearch/model/callbacks' Mongoid.logger.level = Logger::DEBUG Moped.logger.level = Logger::DEBUG Mongoid.connect_to 'articles' Elasticsearch::Model.client = Elasticsearch::Client.new host: 'localhost:9200', log: true class Article include Mongoid::Document field :id, type: String field :title, type: String field :published_at, type: DateTime attr_accessible :id, :title, :published_at if respond_to? :attr_accessible def as_indexed_json(options={}) as_json(except: [:id, :_id]) end end # Extend the model with Elasticsearch support # Article.__send__ :include, Elasticsearch::Model # Article.__send__ :include, Elasticsearch::Model::Callbacks # Store data # Article.delete_all Article.create id: '1', title: 'Foo' Article.create id: '2', title: 'Bar' Article.create id: '3', title: 'Foo Foo' # Index data # client = Elasticsearch::Client.new host:'localhost:9200', log:true client.indices.delete index: 'articles' rescue nil client.bulk index: 'articles', type: 'article', body: Article.all.map { |a| { index: { _id: a.id, data: a.attributes } } }, refresh: true # puts Benchmark.realtime { 9_875.times { |i| Article.create title: "Foo #{i}" } } puts '', '-'*Pry::Terminal.width! response = Article.search 'foo'; Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.to_a'), quiet: true) elasticsearch-model-7.2.1/examples/ohm_article.rb0000644000004100000410000000521514217570270022122 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Ohm for Redis and Elasticsearch # =============================== # # https://github.com/soveran/ohm#example $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__) require 'logger' require 'ansi/core' require 'active_model' require 'ohm' require 'elasticsearch/model' class Article < Ohm::Model # Include JSON serialization from ActiveModel include ActiveModel::Serializers::JSON attribute :title attribute :published_at end # Extend the model with Elasticsearch support # Article.__send__ :include, Elasticsearch::Model # Register a custom adapter # module Elasticsearch module Model module Adapter module Ohm Adapter.register self, lambda { |klass| defined?(::Ohm::Model) and klass.ancestors.include?(::Ohm::Model) } module Records def records klass.fetch(@ids) end end end end end end # Configure the Elasticsearch client to log operations # Elasticsearch::Model.client = Elasticsearch::Client.new log: true puts '', '-'*Pry::Terminal.width! Article.all.map { |a| a.delete } Article.create id: '1', title: 'Foo' Article.create id: '2', title: 'Bar' Article.create id: '3', title: 'Foo Foo' Article.__elasticsearch__.client.indices.delete index: 'articles' rescue nil Article.__elasticsearch__.client.bulk index: 'articles', type: 'article', body: Article.all.map { |a| { index: { _id: a.id, data: a.attributes } } }, refresh: true response = Article.search 'foo', index: 'articles', type: 'article'; Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.to_a'), quiet: true) elasticsearch-model-7.2.1/examples/activerecord_mapping_edge_ngram.rb0000644000004100000410000000713214217570270026171 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'ansi' require 'sqlite3' require 'active_record' require 'elasticsearch/model' ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" ) ActiveRecord::Schema.define(version: 1) do create_table :articles do |t| t.string :title t.date :published_at t.timestamps end end class Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks article_es_settings = { index: { analysis: { filter: { autocomplete_filter: { type: "edge_ngram", min_gram: 1, max_gram: 20 } }, analyzer:{ autocomplete: { type: "custom", tokenizer: "standard", filter: ["lowercase", "autocomplete_filter"] } } } } } settings article_es_settings do mapping do indexes :title indexes :suggestable_title, type: 'string', analyzer: 'autocomplete' end end def as_indexed_json(options={}) as_json.merge(suggestable_title: title) end end Article.__elasticsearch__.client = Elasticsearch::Client.new log: true # Create index Article.__elasticsearch__.create_index! force: true # Store data Article.delete_all Article.create title: 'Foo' Article.create title: 'Bar' Article.create title: 'Foo Foo' Article.__elasticsearch__.refresh_index! # Search and suggest fulltext_search_response = Article.search(query: { match: { title: 'foo'} } ) puts "", "Article search for 'foo':".ansi(:bold), fulltext_search_response.to_a.map { |d| "Title: #{d.title}" }.inspect.ansi(:bold, :yellow), "" fulltext_search_response_2 = Article.search(query: { match: { title: 'fo'} } ) puts "", "Article search for 'fo':".ansi(:bold), fulltext_search_response_2.to_a.map { |d| "Title: #{d.title}" }.inspect.ansi(:bold, :red), "" autocomplete_search_response = Article.search(query: { match: { suggestable_title: { query: 'fo', analyzer: 'standard'} } } ) puts "", "Article autocomplete for 'fo':".ansi(:bold), autocomplete_search_response.to_a.map { |d| "Title: #{d.suggestable_title}" }.inspect.ansi(:bold, :green), "" puts "", "Text 'Foo Bar' analyzed with the default analyzer:".ansi(:bold), Article.__elasticsearch__.client.indices.analyze( index: Article.__elasticsearch__.index_name, field: 'title', text: 'Foo Bar')['tokens'].map { |t| t['token'] }.inspect.ansi(:bold, :yellow), "" puts "", "Text 'Foo Bar' analyzed with the autocomplete filter:".ansi(:bold), Article.__elasticsearch__.client.indices.analyze( index: Article.__elasticsearch__.index_name, field: 'suggestable_title', text: 'Foo Bar')['tokens'].map { |t| t['token'] }.inspect.ansi(:bold, :yellow), "" require 'pry'; binding.pry; elasticsearch-model-7.2.1/examples/activerecord_mapping_completion.rb0000644000004100000410000000454014217570270026252 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'ansi' require 'active_record' require 'elasticsearch/model' ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" ) ActiveRecord::Schema.define(version: 1) do create_table :articles do |t| t.string :title t.date :published_at t.timestamps end end class Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks mapping do indexes :title, type: 'text' do indexes :suggest, type: 'completion' end indexes :url, type: 'keyword' end def as_indexed_json(options={}) as_json.merge 'url' => "/articles/#{id}" end end Article.__elasticsearch__.client = Elasticsearch::Client.new log: true # Create index Article.__elasticsearch__.create_index! force: true # Store data Article.delete_all Article.create title: 'Foo' Article.create title: 'Bar' Article.create title: 'Foo Foo' Article.__elasticsearch__.refresh_index! # Search and suggest response_1 = Article.search 'foo'; puts "Article search:".ansi(:bold), response_1.to_a.map { |d| "Title: #{d.title}" }.inspect.ansi(:bold, :yellow) response_2 = Article.search \ query: { match: { title: 'foo' } }, suggest: { articles: { text: 'foo', completion: { field: 'title.suggest' } } }, _source: ['title', 'url'] puts "Article search with suggest:".ansi(:bold), response_2.response['suggest']['articles'].first['options'].map { |d| "#{d['text']} -> #{d['_source']['url']}" }. inspect.ansi(:bold, :blue) require 'pry'; binding.pry; elasticsearch-model-7.2.1/examples/activerecord_article.rb0000644000004100000410000000541414217570270024012 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ActiveRecord and Elasticsearch # ============================== # # https://github.com/rails/rails/tree/master/activerecord $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__) require 'logger' require 'ansi/core' require 'active_record' require 'kaminari' require 'elasticsearch/model' ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" ) ActiveRecord::Schema.define(version: 1) do create_table :articles do |t| t.string :title t.date :published_at t.timestamps end end Kaminari::Hooks.init if defined?(Kaminari::Hooks) if defined?(Kaminari::Hooks) class Article < ActiveRecord::Base end # Store data # Article.delete_all Article.create title: 'Foo' Article.create title: 'Bar' Article.create title: 'Foo Foo' # Index data # client = Elasticsearch::Client.new log:true # client.indices.delete index: 'articles' rescue nil # client.indices.create index: 'articles', body: { mappings: { article: { dynamic: 'strict' }, properties: {} } } client.indices.delete index: 'articles' rescue nil client.bulk index: 'articles', type: 'article', body: Article.all.as_json.map { |a| { index: { _id: a.delete('id'), data: a } } }, refresh: true # Extend the model with Elasticsearch support # Article.__send__ :include, Elasticsearch::Model # Article.__send__ :include, Elasticsearch::Model::Callbacks # ActiveRecord::Base.logger.silence do # 10_000.times do |i| # Article.create title: "Foo #{i}" # end # end puts '', '-'*Pry::Terminal.width! Elasticsearch::Model.client = Elasticsearch::Client.new log: true response = Article.search 'foo'; p response.size p response.results.size p response.records.size Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.to_a'), quiet: true) elasticsearch-model-7.2.1/examples/datamapper_article.rb0000644000004100000410000000503214217570270023452 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # DataMapper and Elasticsearch # ============================ # # https://github.com/datamapper/dm-core # https://github.com/datamapper/dm-active_model $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__) require 'logger' require 'ansi/core' require 'data_mapper' require 'dm-active_model' require 'active_support/all' require 'elasticsearch/model' DataMapper::Logger.new(STDOUT, :debug) DataMapper.setup(:default, 'sqlite::memory:') class Article include DataMapper::Resource property :id, Serial property :title, String property :published_at, DateTime end DataMapper.auto_migrate! DataMapper.finalize Article.create title: 'Foo' Article.create title: 'Bar' Article.create title: 'Foo Foo' # Extend the model with Elasticsearch support # Article.__send__ :include, Elasticsearch::Model # The DataMapper adapter # module DataMapperAdapter # Implement the interface for fetching records # module Records def records klass.all(id: ids) end # ... end module Callbacks def self.included(model) model.class_eval do after(:create) { __elasticsearch__.index_document } after(:save) { __elasticsearch__.update_document } after(:destroy) { __elasticsearch__.delete_document } end end end end # Register the adapter # Elasticsearch::Model::Adapter.register( DataMapperAdapter, lambda { |klass| defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource) } ) response = Article.search 'foo'; Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.to_a'), quiet: true) elasticsearch-model-7.2.1/examples/riak_article.rb0000644000004100000410000000422314217570270022263 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Riak and Elasticsearch # ====================== # # https://github.com/basho-labs/ripple $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__) require 'logger' require 'ripple' require 'elasticsearch/model' # Documents are stored as JSON objects in Riak but have rich # semantics, including validations and associations. class Article include Ripple::Document property :title, String property :published_at, Time, :default => proc { Time.now } end # Extend the model with Elasticsearch support # Article.__send__ :include, Elasticsearch::Model # Create documents in Riak # Article.destroy_all Article.create id: '1', title: 'Foo' Article.create id: '2', title: 'Bar' Article.create id: '3', title: 'Foo Foo' # Index data into Elasticsearch # client = Elasticsearch::Client.new log:true client.indices.delete index: 'articles' rescue nil client.bulk index: 'articles', type: 'article', body: Article.all.map { |a| { index: { _id: a.key, data: JSON.parse(a.robject.raw_data) } } }.as_json, refresh: true response = Article.search 'foo'; Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.to_a'), quiet: true) elasticsearch-model-7.2.1/examples/activerecord_custom_analyzer.rb0000644000004100000410000001116114217570270025602 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Custom Analyzer for ActiveRecord integration with Elasticsearch # =============================================================== $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'ansi' require 'logger' require 'active_record' require 'elasticsearch/model' ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" ) ActiveRecord::Schema.define(version: 1) do create_table :articles do |t| t.string :title t.date :published_at t.timestamps end end Elasticsearch::Model.client.transport.transport.logger = ActiveSupport::Logger.new(STDOUT) Elasticsearch::Model.client.transport.transport.logger.formatter = lambda { |s, d, p, m| "#{m.ansi(:faint)}\n" } class Article < ActiveRecord::Base include Elasticsearch::Model settings index: { number_of_shards: 1, number_of_replicas: 0, analysis: { analyzer: { pattern: { type: 'pattern', pattern: "\\s|_|-|\\.", lowercase: true }, trigram: { tokenizer: 'trigram' } }, tokenizer: { trigram: { type: 'ngram', min_gram: 3, max_gram: 3, token_chars: ['letter', 'digit'] } } } } do mapping do indexes :title, type: 'text', analyzer: 'english' do indexes :keyword, analyzer: 'keyword' indexes :pattern, analyzer: 'pattern' indexes :trigram, analyzer: 'trigram' end end end end # Create example records # Article.delete_all Article.create title: 'Foo' Article.create title: 'Foo-Bar' Article.create title: 'Foo_Bar_Bazooka' Article.create title: 'Foo.Bar' # Index records # errors = Article.import force: true, refresh: true, return: 'errors' puts "[!] Errors importing records: #{errors.map { |d| d['index']['error'] }.join(', ')}".ansi(:red) && exit(1) unless errors.empty? puts '', '-'*80 puts "English analyzer [Foo_Bar_1_Bazooka]".ansi(:bold), "Tokens: " + Article.__elasticsearch__.client.indices .analyze(index: Article.index_name, body: { field: 'title', text: 'Foo_Bar_1_Bazooka' })['tokens'] .map { |d| "[#{d['token']}]" }.join(' '), "\n" puts "Keyword analyzer [Foo_Bar_1_Bazooka]".ansi(:bold), "Tokens: " + Article.__elasticsearch__.client.indices .analyze(index: Article.index_name, body: { field: 'title.keyword', text: 'Foo_Bar_1_Bazooka' })['tokens'] .map { |d| "[#{d['token']}]" }.join(' '), "\n" puts "Pattern analyzer [Foo_Bar_1_Bazooka]".ansi(:bold), "Tokens: " + Article.__elasticsearch__.client.indices .analyze(index: Article.index_name, body: { field: 'title.pattern', text: 'Foo_Bar_1_Bazooka' })['tokens'] .map { |d| "[#{d['token']}]" }.join(' '), "\n" puts "Trigram analyzer [Foo_Bar_1_Bazooka]".ansi(:bold), "Tokens: " + Article.__elasticsearch__.client.indices .analyze(index: Article.index_name, body: { field: 'title.trigram', text: 'Foo_Bar_1_Bazooka' })['tokens'] .map { |d| "[#{d['token']}]" }.join(' '), "\n" puts '', '-'*80 response = Article.search query: { match: { 'title' => 'foo' } } ; puts "English search for 'foo'".ansi(:bold), "#{response.response.hits.total} matches: " + response.records.map { |d| d.title }.join(', '), "\n" puts '', '-'*80 response = Article.search query: { match: { 'title.pattern' => 'foo' } } ; puts "Pattern search for 'foo'".ansi(:bold), "#{response.response.hits.total} matches: " + response.records.map { |d| d.title }.join(', '), "\n" puts '', '-'*80 response = Article.search query: { match: { 'title.trigram' => 'zoo' } } ; puts "Trigram search for 'zoo'".ansi(:bold), "#{response.response.hits.total} matches: " + response.records.map { |d| d.title }.join(', '), "\n" puts '', '-'*80 require 'pry'; binding.pry; elasticsearch-model-7.2.1/examples/activerecord_associations.rb0000644000004100000410000001575114217570270025073 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ActiveRecord associations and Elasticsearch # =========================================== # # https://github.com/rails/rails/tree/master/activerecord # http://guides.rubyonrails.org/association_basics.html # # Run me with: # # ruby -I lib examples/activerecord_associations.rb # $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'pry' require 'logger' require 'ansi/core' require 'active_record' require 'json' require 'elasticsearch/model' ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" ) # ----- Schema definition ------------------------------------------------------------------------- ActiveRecord::Schema.define(version: 1) do create_table :categories do |t| t.string :title t.timestamps null: false end create_table :authors do |t| t.string :first_name, :last_name t.string :department t.timestamps null: false end create_table :authorships do |t| t.references :article t.references :author t.timestamps null: false end create_table :articles do |t| t.string :title t.timestamps null: false end create_table :articles_categories, id: false do |t| t.references :article, :category end create_table :comments do |t| t.string :text t.references :article t.timestamps null: false end add_index(:comments, :article_id) unless index_exists?(:comments, :article_id) end # ----- Elasticsearch client setup ---------------------------------------------------------------- Elasticsearch::Model.client = Elasticsearch::Client.new log: true Elasticsearch::Model.client.transport.transport.logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" } # ----- Search integration ------------------------------------------------------------------------ module Searchable extend ActiveSupport::Concern included do include Elasticsearch::Model include Elasticsearch::Model::Callbacks include Indexing after_touch() { __elasticsearch__.index_document } end module Indexing #Index only the specified fields settings do mappings dynamic: false do indexes :categories, type: :object do indexes :title end indexes :authors, type: :object do indexes :full_name indexes :department end indexes :comments, type: :object do indexes :text end end end # Customize the JSON serialization for Elasticsearch def as_indexed_json(options={}) self.as_json( include: { categories: { only: :title}, authors: { methods: [:full_name, :department], only: [:full_name, :department] }, comments: { only: :text } }) end end end # ----- Model definitions ------------------------------------------------------------------------- class Category < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks has_and_belongs_to_many :articles end class Author < ActiveRecord::Base has_many :authorships after_update { self.authorships.each(&:touch) } def full_name [first_name, last_name].compact.join(' ') end end class Authorship < ActiveRecord::Base belongs_to :author belongs_to :article, touch: true end class Article < ActiveRecord::Base include Searchable has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] has_many :authorships has_many :authors, through: :authorships has_many :comments end class Comment < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks belongs_to :article, touch: true end # ----- Insert data ------------------------------------------------------------------------------- # Create category # category = Category.create title: 'One' # Create author # author = Author.create first_name: 'John', last_name: 'Smith', department: 'Business' # Create article article = Article.create title: 'First Article' # Assign category # article.categories << category # Assign author # article.authors << author # Add comment # article.comments.create text: 'First comment for article One' article.comments.create text: 'Second comment for article One' Elasticsearch::Model.client.indices.refresh index: Elasticsearch::Model::Registry.all.map(&:index_name) # Search for a term and return records # puts "", "Articles containing 'one':".ansi(:bold), Article.search('one').records.to_a.map(&:inspect), "" puts "", "All Models containing 'one':".ansi(:bold), Elasticsearch::Model.search('one').records.to_a.map(&:inspect), "" # Difference between `records` and `results` # response = Article.search query: { match: { title: 'first' } } puts "", "Search results are wrapped in the <#{response.class}> class", "" puts "", "Access the instances with the `#records` method:".ansi(:bold), response.records.map { |r| "* #{r.title} | Authors: #{r.authors.map(&:full_name) } | Comment count: #{r.comments.size}" }.join("\n"), "" puts "", "Access the Elasticsearch documents with the `#results` method (without touching the database):".ansi(:bold), response.results.map { |r| "* #{r.title} | Authors: #{r.authors.map(&:full_name) } | Comment count: #{r.comments.size}" }.join("\n"), "" puts "", "The whole indexed document (according to `Article#as_indexed_json`):".ansi(:bold), JSON.pretty_generate(response.results.first._source.to_hash), "" # Retrieve only selected fields from Elasticsearch # response = Article.search query: { match: { title: 'first' } }, _source: ['title', 'authors.full_name'] puts "", "Retrieve only selected fields from Elasticsearch:".ansi(:bold), JSON.pretty_generate(response.results.first._source.to_hash), "" # ----- Pry --------------------------------------------------------------------------------------- Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' }, input: StringIO.new('response.records.first'), quiet: true) elasticsearch-model-7.2.1/Rakefile0000644000004100000410000000516514217570270017142 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'bundler/gem_tasks' desc 'Run unit tests' task default: 'test:all' task test: 'test:all' gemfiles = ['5.0.gemfile', '6.0.gemfile'] gemfiles << '4.0.gemfile' if RUBY_VERSION < '2.7' GEMFILES = gemfiles.freeze namespace :bundle do desc 'Install dependencies for all the Gemfiles in /gemfiles. Optionally define env variable RAILS_VERSIONS. E.g. RAILS_VERSIONS=3.0,5.0' task :install do unless defined?(JRUBY_VERSION) puts '-' * 80 gemfiles = ENV['RAILS_VERSIONS'] ? ENV['RAILS_VERSIONS'].split(',').map { |v| "#{v}.gemfile"} : GEMFILES gemfiles.each do |gemfile| puts "GEMFILE: #{gemfile}" Bundler.with_unbundled_env do sh "bundle install --gemfile #{File.expand_path('../gemfiles/'+gemfile, __FILE__)}" end puts '-' * 80 end end end end # ----- Test tasks ------------------------------------------------------------ require 'rake/testtask' namespace :test do desc 'Run all tests. Optionally define env variable RAILS_VERSIONS. E.g. RAILS_VERSIONS=3.0,5.0' task :all, [:rails_versions] do |task, args| gemfiles = ENV['RAILS_VERSIONS'] ? ENV['RAILS_VERSIONS'].split(',').map {|v| "#{v}.gemfile"} : GEMFILES puts '-' * 80 gemfiles.each do |gemfile| puts "GEMFILE: #{gemfile}" sh "BUNDLE_GEMFILE='#{File.expand_path("../gemfiles/#{gemfile}", __FILE__)}' " + " bundle exec rspec" puts '-' * 80 end end end # ----- Documentation tasks --------------------------------------------------- require 'yard' YARD::Rake::YardocTask.new(:doc) do |t| t.options = %w| --embed-mixins --markup=markdown | end # ----- Code analysis tasks --------------------------------------------------- if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9' require 'cane/rake_task' Cane::RakeTask.new(:quality) do |cane| cane.abc_max = 15 cane.no_style = true end end elasticsearch-model-7.2.1/lib/0000755000004100000410000000000014217570270016234 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/0000755000004100000410000000000014217570270021046 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/model.rb0000644000004100000410000001544114217570270022500 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'hashie/mash' require 'active_support/core_ext/module/delegation' require 'elasticsearch' require 'elasticsearch/model/version' require 'elasticsearch/model/hash_wrapper' require 'elasticsearch/model/client' require 'elasticsearch/model/multimodel' require 'elasticsearch/model/adapter' require 'elasticsearch/model/adapters/default' require 'elasticsearch/model/adapters/active_record' require 'elasticsearch/model/adapters/mongoid' require 'elasticsearch/model/adapters/multiple' require 'elasticsearch/model/importing' require 'elasticsearch/model/indexing' require 'elasticsearch/model/naming' require 'elasticsearch/model/serializing' require 'elasticsearch/model/searching' require 'elasticsearch/model/callbacks' require 'elasticsearch/model/proxy' require 'elasticsearch/model/response' require 'elasticsearch/model/response/base' require 'elasticsearch/model/response/result' require 'elasticsearch/model/response/results' require 'elasticsearch/model/response/records' require 'elasticsearch/model/response/pagination' require 'elasticsearch/model/response/aggregations' require 'elasticsearch/model/response/suggestions' require 'elasticsearch/model/ext/active_record' case when defined?(::Kaminari) Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari when defined?(::WillPaginate) Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::WillPaginate end module Elasticsearch # Elasticsearch integration for Ruby models # ========================================= # # `Elasticsearch::Model` contains modules for integrating the Elasticsearch search and analytical engine # with ActiveModel-based classes, or models, for the Ruby programming language. # # It facilitates importing your data into an index, automatically updating it when a record changes, # searching the specific index, setting up the index mapping or the model JSON serialization. # # When the `Elasticsearch::Model` module is included in your class, it automatically extends it # with the functionality; see {Elasticsearch::Model.included}. Most methods are available via # the `__elasticsearch__` class and instance method proxies. # # It is possible to include/extend the model with the corresponding # modules directly, if that is desired: # # MyModel.__send__ :extend, Elasticsearch::Model::Client::ClassMethods # MyModel.__send__ :include, Elasticsearch::Model::Client::InstanceMethods # MyModel.__send__ :extend, Elasticsearch::Model::Searching::ClassMethods # # ... # module Model METHODS = [:search, :mapping, :mappings, :settings, :index_name, :document_type, :import] # Adds the `Elasticsearch::Model` functionality to the including class. # # * Creates the `__elasticsearch__` class and instance method. These methods return a proxy object with # other common methods defined on them. # * The module includes other modules with further functionality. # * Sets up delegation for common methods such as `import` and `search`. # # @example Include the module in the `Article` model definition # # class Article < ActiveRecord::Base # include Elasticsearch::Model # end # # @example Inject the module into the `Article` model during run time # # Article.__send__ :include, Elasticsearch::Model # # def self.included(base) base.class_eval do include Elasticsearch::Model::Proxy # Delegate common methods to the `__elasticsearch__` ClassMethodsProxy, unless they are defined already class << self METHODS.each do |method| delegate method, to: :__elasticsearch__ unless self.public_instance_methods.include?(method) end end end # Add to the model to the registry if it's a class (and not in intermediate module) Registry.add(base) if base.is_a?(Class) end module ClassMethods # Get the client common for all models # # @example Get the client # # Elasticsearch::Model.client # => # # def client @client ||= Elasticsearch::Client.new end # Set the client for all models # # @example Configure (set) the client for all models # # Elasticsearch::Model.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true # => # # # @note You have to set the client before you call Elasticsearch methods on the model, # or set it directly on the model; see {Elasticsearch::Model::Client::ClassMethods#client} # def client=(client) @client = client end # Search across multiple models # # By default, all models which include the `Elasticsearch::Model` module are searched # # @param query_or_payload [String,Hash,Object] The search request definition # (string, JSON, Hash, or object responding to `to_hash`) # @param models [Array] The Array of Model objects to search # @param options [Hash] Optional parameters to be passed to the Elasticsearch client # # @return [Elasticsearch::Model::Response::Response] # # @example Search across specific models # # Elasticsearch::Model.search('foo', [Author, Article]) # # @example Search across all models which include the `Elasticsearch::Model` module # # Elasticsearch::Model.search('foo') # def search(query_or_payload, models=[], options={}) models = Multimodel.new(models) request = Searching::SearchRequest.new(models, query_or_payload, options) Response::Response.new(models, request) end # Access the module settings # def settings @settings ||= {} end end extend ClassMethods class NotImplemented < NoMethodError; end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/0000755000004100000410000000000014217570270022146 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/model/proxy.rb0000644000004100000410000001512314217570270023656 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # This module provides a proxy interfacing between the including class and # `Elasticsearch::Model`, preventing the pollution of the including class namespace. # # The only "gateway" between the model and Elasticsearch::Model is the # `#__elasticsearch__` class and instance method. # # The including class must be compatible with # [ActiveModel](https://github.com/rails/rails/tree/master/activemodel). # # @example Include the `Elasticsearch::Model` module into an `Article` model # # class Article < ActiveRecord::Base # include Elasticsearch::Model # end # # Article.__elasticsearch__.respond_to?(:search) # # => true # # article = Article.first # # article.respond_to? :index_document # # => false # # article.__elasticsearch__.respond_to?(:index_document) # # => true # module Proxy # Define the `__elasticsearch__` class and instance methods in the including class # and register a callback for intercepting changes in the model. # # @note The callback is triggered only when `Elasticsearch::Model` is included in the # module and the functionality is accessible via the proxy. # def self.included(base) base.class_eval do # `ClassMethodsProxy` instance, accessed as `MyModel.__elasticsearch__` def self.__elasticsearch__ &block @__elasticsearch__ ||= ClassMethodsProxy.new(self) @__elasticsearch__.instance_eval(&block) if block_given? @__elasticsearch__ end # Mix the importing module into the `ClassMethodsProxy` self.__elasticsearch__.class_eval do include Adapter.from_class(base).importing_mixin end # Register a callback for storing changed attributes for models which implement # `before_save` method and return changed attributes (ie. when `Elasticsearch::Model` is included) # # @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html # before_save do |obj| if obj.respond_to?(:changes_to_save) # Rails 5.1 changes_to_save = obj.changes_to_save elsif obj.respond_to?(:changes) changes_to_save = obj.changes end if changes_to_save attrs = obj.__elasticsearch__.instance_variable_get(:@__changed_model_attributes) || {} latest_changes = changes_to_save.inject({}) { |latest_changes, (k,v)| latest_changes.merge!(k => v.last) } obj.__elasticsearch__.instance_variable_set(:@__changed_model_attributes, attrs.merge(latest_changes)) end end if respond_to?(:before_save) end # {InstanceMethodsProxy}, accessed as `@mymodel.__elasticsearch__` # def __elasticsearch__ &block @__elasticsearch__ ||= InstanceMethodsProxy.new(self) @__elasticsearch__.instance_eval(&block) if block_given? @__elasticsearch__ end end # @overload dup # # Returns a copy of this object. Resets the __elasticsearch__ proxy so # the duplicate will build its own proxy. def initialize_dup(_) @__elasticsearch__ = nil super end # Common module for the proxy classes # module Base attr_reader :target def initialize(target) @target = target end def ruby2_keywords(*) # :nodoc: end if RUBY_VERSION < "2.7" # Delegate methods to `@target`. As per [the Ruby 3.0 explanation for keyword arguments](https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/), the only way to work on Ruby <2.7, and 2.7, and 3.0+ is to use `ruby2_keywords`. # ruby2_keywords def method_missing(method_name, *arguments, &block) target.respond_to?(method_name) ? target.__send__(method_name, *arguments, &block) : super end # Respond to methods from `@target` # def respond_to_missing?(method_name, include_private = false) target.respond_to?(method_name) || super end def inspect "[PROXY] #{target.inspect}" end end # A proxy interfacing between Elasticsearch::Model class methods and model class methods # # TODO: Inherit from BasicObject and make Pry's `ls` command behave? # class ClassMethodsProxy include Base include Elasticsearch::Model::Client::ClassMethods include Elasticsearch::Model::Naming::ClassMethods include Elasticsearch::Model::Indexing::ClassMethods include Elasticsearch::Model::Searching::ClassMethods include Elasticsearch::Model::Importing::ClassMethods end # A proxy interfacing between Elasticsearch::Model instance methods and model instance methods # # TODO: Inherit from BasicObject and make Pry's `ls` command behave? # class InstanceMethodsProxy include Base include Elasticsearch::Model::Client::InstanceMethods include Elasticsearch::Model::Naming::InstanceMethods include Elasticsearch::Model::Indexing::InstanceMethods include Elasticsearch::Model::Serializing::InstanceMethods def klass target.class end def class klass.__elasticsearch__ end # Need to redefine `as_json` because we're not inheriting from `BasicObject`; # see TODO note above. # def as_json(options={}) target.as_json(options) end def as_indexed_json(options={}) target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/version.rb0000644000004100000410000000151414217570270024161 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model VERSION = "7.2.1" end end elasticsearch-model-7.2.1/lib/elasticsearch/model/adapters/0000755000004100000410000000000014217570270023751 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/model/adapters/default.rb0000644000004100000410000000415514217570270025727 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Adapter # The default adapter for models which haven't one registered # module Default # Module for implementing methods and logic related to fetching records from the database # module Records # Return the collection of records fetched from the database # # By default uses `MyModel#find[1, 2, 3]` # def records klass.find(@ids) end end # Module for implementing methods and logic related to hooking into model lifecycle # (e.g. to perform automatic index updates) # # @see http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html module Callbacks # noop end # Module for efficiently fetching records from the database to import them into the index # module Importing # @abstract Implement this method in your adapter # def __find_in_batches(options={}, &block) raise NotImplemented, "Method not implemented for default adapter" end # @abstract Implement this method in your adapter # def __transform raise NotImplemented, "Method not implemented for default adapter" end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/adapters/active_record.rb0000644000004100000410000001022614217570270027110 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Adapter # An adapter for ActiveRecord-based models # module ActiveRecord Adapter.register self, lambda { |klass| !!defined?(::ActiveRecord::Base) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::ActiveRecord::Base) } module Records attr_writer :options def options @options ||= {} end # Returns an `ActiveRecord::Relation` instance # def records sql_records = klass.where(klass.primary_key => ids) sql_records = sql_records.includes(self.options[:includes]) if self.options[:includes] # Re-order records based on the order from Elasticsearch hits # by redefining `to_a`, unless the user has called `order()` # sql_records.instance_exec(response.response['hits']['hits']) do |hits| ar_records_method_name = :to_a ar_records_method_name = :records if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 5 define_singleton_method(ar_records_method_name) do if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 self.load else self.__send__(:exec_queries) end if !self.order_values.present? @records.sort_by { |record| hits.index { |hit| hit['_id'].to_s == record.id.to_s } } else @records end end if self end sql_records end # Prevent clash with `ActiveSupport::Dependencies::Loadable` # def load records.__send__(:load) end end module Callbacks # Handle index updates (creating, updating or deleting documents) # when the model changes, by hooking into the lifecycle # # @see http://guides.rubyonrails.org/active_record_callbacks.html # def self.included(base) base.class_eval do after_commit lambda { __elasticsearch__.index_document }, on: :create after_commit lambda { __elasticsearch__.update_document }, on: :update after_commit lambda { __elasticsearch__.delete_document }, on: :destroy end end end module Importing # Fetch batches of records from the database (used by the import method) # # # @see http://api.rubyonrails.org/classes/ActiveRecord/Batches.html ActiveRecord::Batches.find_in_batches # def __find_in_batches(options={}, &block) query = options.delete(:query) named_scope = options.delete(:scope) preprocess = options.delete(:preprocess) scope = self scope = scope.__send__(named_scope) if named_scope scope = scope.instance_exec(&query) if query scope.find_in_batches(**options) do |batch| batch = self.__send__(preprocess, batch) if preprocess yield(batch) if batch.present? end end def __transform lambda { |model| { index: { _id: model.id, data: model.__elasticsearch__.as_indexed_json } } } end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/adapters/mongoid.rb0000644000004100000410000000700314217570270025732 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Adapter # An adapter for Mongoid-based models # # @see http://mongoid.org # module Mongoid Adapter.register self, lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) } module Records # Return a `Mongoid::Criteria` instance # def records criteria = klass.where(:id.in => ids) criteria.instance_exec(response.response['hits']['hits']) do |hits| define_singleton_method :to_a do self.entries.sort_by { |e| hits.index { |hit| hit['_id'].to_s == e.id.to_s } } end end criteria end # Intercept call to sorting methods, so we can ignore the order from Elasticsearch # %w| asc desc order_by |.each do |name| define_method name do |*args| criteria = records.__send__ name, *args criteria.instance_exec do define_singleton_method(:to_a) { self.entries } end criteria end end end module Callbacks # Handle index updates (creating, updating or deleting documents) # when the model changes, by hooking into the lifecycle # # @see http://mongoid.org/en/mongoid/docs/callbacks.html # def self.included(base) base.after_create { |document| document.__elasticsearch__.index_document } base.after_update { |document| document.__elasticsearch__.update_document } base.after_destroy { |document| document.__elasticsearch__.delete_document } end end module Importing # Fetch batches of records from the database # # @see https://github.com/mongoid/mongoid/issues/1334 # @see https://github.com/karmi/retire/pull/724 # def __find_in_batches(options={}, &block) batch_size = options[:batch_size] || 1_000 query = options[:query] named_scope = options[:scope] preprocess = options[:preprocess] scope = all scope = scope.send(named_scope) if named_scope scope = query.is_a?(Proc) ? scope.class_exec(&query) : scope.where(query) if query scope.no_timeout.each_slice(batch_size) do |items| yield (preprocess ? self.__send__(preprocess, items) : items) end end def __transform lambda {|a| { index: { _id: a.id.to_s, data: a.as_indexed_json } }} end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/adapters/multiple.rb0000644000004100000410000001060214217570270026130 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Adapter # An adapter to be used for deserializing results from multiple models, # retrieved through `Elasticsearch::Model.search` # # @see Elasticsearch::Model.search # module Multiple Adapter.register self, lambda { |klass| klass.is_a? Multimodel } module Records # Returns a collection of model instances, possibly of different classes (ActiveRecord, Mongoid, ...) # # @note The order of results in the Elasticsearch response is preserved # def records records_by_type = __records_by_type records = response.response["hits"]["hits"].map do |hit| records_by_type[ __type_for_hit(hit) ][ hit[:_id] ] end records.compact end # Returns the collection of records grouped by class based on `_type` # # Example: # # { # Foo => {"1"=> # {"1"=> # ids) when Elasticsearch::Model::Adapter::Mongoid.equal?(adapter) klass.where(:id.in => ids) else klass.find(ids) end end # Returns the record IDs grouped by class based on type `_type` # # Example: # # { Foo => ["1"], Bar => ["1", "5"] } # # @api private # def __ids_by_type ids_by_type = {} response.response["hits"]["hits"].each do |hit| type = __type_for_hit(hit) ids_by_type[type] ||= [] ids_by_type[type] << hit[:_id] end ids_by_type end # Returns the class of the model corresponding to a specific `hit` in Elasticsearch results # # @see Elasticsearch::Model::Registry # # @api private # def __type_for_hit(hit) @@__types ||= {} key = "#{hit[:_index]}::#{hit[:_type]}" if hit[:_type] && hit[:_type] != '_doc' key = hit[:_index] unless key @@__types[key] ||= begin Registry.all.detect do |model| (model.index_name == hit[:_index] && __no_type?(hit)) || (model.index_name == hit[:_index] && model.document_type == hit[:_type]) end end end def __no_type?(hit) hit[:_type].nil? || hit[:_type] == '_doc' end # Returns the adapter registered for a particular `klass` or `nil` if not available # # @api private # def __adapter_for_klass(klass) Adapter.adapters.select { |name, checker| checker.call(klass) }.keys.first end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/adapter.rb0000644000004100000410000001176714217570270024127 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Contains an adapter which provides OxM-specific implementations for common behaviour: # # * {Adapter::Adapter#records_mixin Fetching records from the database} # * {Adapter::Adapter#callbacks_mixin Model callbacks for automatic index updates} # * {Adapter::Adapter#importing_mixin Efficient bulk loading from the database} # # @see Elasticsearch::Model::Adapter::Default # @see Elasticsearch::Model::Adapter::ActiveRecord # @see Elasticsearch::Model::Adapter::Mongoid # module Adapter # Returns an adapter based on the Ruby class passed # # @example Create an adapter for an ActiveRecord-based model # # class Article < ActiveRecord::Base; end # # myadapter = Elasticsearch::Model::Adapter.from_class(Article) # myadapter.adapter # # => Elasticsearch::Model::Adapter::ActiveRecord # # @see Adapter.adapters The list of included adapters # @see Adapter.register Register a custom adapter # def from_class(klass) Adapter.new(klass) end; module_function :from_class # Returns registered adapters # # @see ::Elasticsearch::Model::Adapter::Adapter.adapters # def adapters Adapter.adapters end; module_function :adapters # Registers an adapter # # @see ::Elasticsearch::Model::Adapter::Adapter.register # def register(name, condition) Adapter.register(name, condition) end; module_function :register # Contains an adapter for specific OxM or architecture. # class Adapter attr_reader :klass def initialize(klass) @klass = klass end # Registers an adapter for specific condition # # @param name [Module] The module containing the implemented interface # @param condition [Proc] An object with a `call` method which is evaluated in {.adapter} # # @example Register an adapter for DataMapper # # module DataMapperAdapter # # # Implement the interface for fetching records # # # module Records # def records # klass.all(id: @ids) # end # # # ... # end # end # # # Register the adapter # # # Elasticsearch::Model::Adapter.register( # DataMapperAdapter, # lambda { |klass| # defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource) # } # ) # def self.register(name, condition) self.adapters[name] = condition end # Return the collection of registered adapters # # @example Return the currently registered adapters # # Elasticsearch::Model::Adapter.adapters # # => { # # Elasticsearch::Model::Adapter::ActiveRecord => #, # # Elasticsearch::Model::Adapter::Mongoid => #, # # } # # @return [Hash] The collection of adapters # def self.adapters @adapters ||= {} end # Return the module with {Default::Records} interface implementation # # @api private # def records_mixin adapter.const_get(:Records) end # Return the module with {Default::Callbacks} interface implementation # # @api private # def callbacks_mixin adapter.const_get(:Callbacks) end # Return the module with {Default::Importing} interface implementation # # @api private # def importing_mixin adapter.const_get(:Importing) end # Returns the adapter module # # @api private # def adapter @adapter ||= begin self.class.adapters.find( lambda {[]} ) { |name, condition| condition.call(klass) }.first \ || Elasticsearch::Model::Adapter::Default end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/0000755000004100000410000000000014217570270024004 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/model/response/pagination.rb0000644000004100000410000000160314217570270026462 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. require 'elasticsearch/model/response/pagination/kaminari' require 'elasticsearch/model/response/pagination/will_paginate' elasticsearch-model-7.2.1/lib/elasticsearch/model/response/pagination/0000755000004100000410000000000014217570270026135 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/model/response/pagination/will_paginate.rb0000644000004100000410000000663514217570270031313 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response # Pagination for search results/records # module Pagination # Allow models to be paginated with the "will_paginate" gem [https://github.com/mislav/will_paginate] # module WillPaginate def self.included(base) base.__send__ :include, ::WillPaginate::CollectionMethods # Include the paging methods in results and records # methods = [:current_page, :offset, :length, :per_page, :total_entries, :total_pages, :previous_page, :next_page, :out_of_bounds?] Elasticsearch::Model::Response::Results.__send__ :delegate, *methods, to: :response Elasticsearch::Model::Response::Records.__send__ :delegate, *methods, to: :response end def offset (current_page - 1) * per_page end def length search.definition[:size] end # Main pagination method # # @example # # Article.search('foo').paginate(page: 1, per_page: 30) # def paginate(options) param_name = options[:param_name] || :page page = [options[param_name].to_i, 1].max per_page = (options[:per_page] || __default_per_page).to_i search.definition.update size: per_page, from: (page - 1) * per_page self end # Return the current page # def current_page search.definition[:from] / per_page + 1 if search.definition[:from] && per_page end # Pagination method # # @example # # Article.search('foo').page(2) # def page(num) paginate(page: num, per_page: per_page) # shorthand end # Return or set the "size" value # # @example # # Article.search('foo').per_page(15).page(2) # def per_page(num = nil) if num.nil? search.definition[:size] else paginate(page: current_page, per_page: num) # shorthand end end # Returns the total number of results # def total_entries results.total end # Returns the models's `per_page` value or the default # # @api private # def __default_per_page klass.respond_to?(:per_page) && klass.per_page || ::WillPaginate.per_page end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/pagination/kaminari.rb0000644000004100000410000001033314217570270030255 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response # Pagination for search results/records # module Pagination # Allow models to be paginated with the "kaminari" gem [https://github.com/amatsuda/kaminari] # module Kaminari def self.included(base) # Include the Kaminari configuration and paging method in response # base.__send__ :include, ::Kaminari::ConfigurationMethods::ClassMethods base.__send__ :include, ::Kaminari::PageScopeMethods # Include the Kaminari paging methods in results and records # Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::ConfigurationMethods::ClassMethods Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::PageScopeMethods Elasticsearch::Model::Response::Records.__send__ :include, ::Kaminari::PageScopeMethods Elasticsearch::Model::Response::Results.__send__ :delegate, :limit_value, :offset_value, :total_count, :max_pages, to: :response Elasticsearch::Model::Response::Records.__send__ :delegate, :limit_value, :offset_value, :total_count, :max_pages, to: :response base.class_eval <<-RUBY, __FILE__, __LINE__ + 1 # Define the `page` Kaminari method # def #{::Kaminari.config.page_method_name}(num=nil) @results = nil @records = nil @response = nil @page = [num.to_i, 1].max @per_page ||= __default_per_page self.search.definition.update size: @per_page, from: @per_page * (@page - 1) self end RUBY end # Returns the current "limit" (`size`) value # def limit_value case when search.definition[:size] search.definition[:size] else __default_per_page end end # Returns the current "offset" (`from`) value # def offset_value case when search.definition[:from] search.definition[:from] else 0 end end # Set the "limit" (`size`) value # def limit(value) return self if value.to_i <= 0 @results = nil @records = nil @response = nil @per_page = value.to_i search.definition.update :size => @per_page search.definition.update :from => @per_page * (@page - 1) if @page self end # Set the "offset" (`from`) value # def offset(value) return self if value.to_i < 0 @results = nil @records = nil @response = nil @page = nil search.definition.update :from => value.to_i self end # Returns the total number of results # def total_count results.total end # Returns the models's `per_page` value or the default # # @api private # def __default_per_page klass.respond_to?(:default_per_page) && klass.default_per_page || ::Kaminari.config.default_per_page end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/aggregations.rb0000644000004100000410000000361114217570270027004 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response class Aggregations < HashWrapper disable_warnings if respond_to?(:disable_warnings) def initialize(attributes={}) __redefine_enumerable_methods super(attributes) end # Fix the problem of Hashie::Mash returning unexpected values for `min` and `max` methods # # People can define names for aggregations such as `min` and `max`, but these # methods are defined in `Enumerable#min` and `Enumerable#max` # # { foo: 'bar' }.min # # => [:foo, "bar"] # # Therefore, any Hashie::Mash instance value has the `min` and `max` # methods redefined to return the real value # def __redefine_enumerable_methods(h) if h.respond_to?(:each_pair) h.each_pair { |k, v| v = __redefine_enumerable_methods(v) } end if h.is_a?(Hashie::Mash) class << h define_method(:min) { self[:min] } define_method(:max) { self[:max] } end end end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/records.rb0000644000004100000410000000517114217570270025776 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response # Encapsulates the collection of records returned from the database # # Implements Enumerable and forwards its methods to the {#records} object, # which is provided by an {Elasticsearch::Model::Adapter::Adapter} implementation. # class Records include Enumerable delegate :each, :empty?, :size, :slice, :[], :to_a, :to_ary, to: :records attr_accessor :options include Base # @see Base#initialize # def initialize(klass, response, options={}) super # Include module provided by the adapter in the singleton class ("metaclass") # adapter = Adapter.from_class(klass) metaclass = class << self; self; end metaclass.__send__ :include, adapter.records_mixin self.options = options end # Returns the hit IDs # def ids response.response['hits']['hits'].map { |hit| hit['_id'] } end # Returns the {Results} collection # def results response.results end # Yields [record, hit] pairs to the block # def each_with_hit(&block) records.to_a.zip(results).each(&block) end # Yields [record, hit] pairs and returns the result # def map_with_hit(&block) records.to_a.zip(results).map(&block) end # Delegate methods to `@records` # def method_missing(method_name, *arguments) records.respond_to?(method_name) ? records.__send__(method_name, *arguments) : super end # Respond to methods from `@records` # def respond_to?(method_name, include_private = false) records.respond_to?(method_name) || super end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/base.rb0000644000004100000410000000414614217570270025250 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response # Common funtionality for classes in the {Elasticsearch::Model::Response} module # module Base attr_reader :klass, :response, :raw_response # @param klass [Class] The name of the model class # @param response [Hash] The full response returned from Elasticsearch client # @param options [Hash] Optional parameters # def initialize(klass, response, options={}) @klass = klass @raw_response = response @response = response end # @abstract Implement this method in specific class # def results raise NotImplemented, "Implement this method in #{klass}" end # @abstract Implement this method in specific class # def records raise NotImplemented, "Implement this method in #{klass}" end # Returns the total number of hits # def total if response.response['hits']['total'].respond_to?(:keys) response.response['hits']['total']['value'] else response.response['hits']['total'] end end # Returns the max_score # def max_score response.response['hits']['max_score'] end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/result.rb0000644000004100000410000000476614217570270025664 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response # Encapsulates the "hit" returned from the Elasticsearch client # # Wraps the raw Hash with in a `Hashie::Mash` instance, providing # access to the Hash properties by calling Ruby methods. # # @see https://github.com/intridea/hashie # class Result # @param attributes [Hash] A Hash with document properties # def initialize(attributes={}) @result = HashWrapper.new(attributes) end # Return document `_id` as `id` # def id @result['_id'] end # Return document `_type` as `_type` # def type @result['_type'] end # Delegate methods to `@result` or `@result._source` # def method_missing(name, *arguments) case when name.to_s.end_with?('?') @result.__send__(name, *arguments) || ( @result._source && @result._source.__send__(name, *arguments) ) when @result.respond_to?(name) @result.__send__ name, *arguments when @result._source && @result._source.respond_to?(name) @result._source.__send__ name, *arguments else super end end # Respond to methods from `@result` or `@result._source` # def respond_to_missing?(method_name, include_private = false) @result.respond_to?(method_name.to_sym) || \ @result._source && @result._source.respond_to?(method_name.to_sym) || \ super end def as_json(options={}) @result.as_json(options) end # TODO: #to_s, #inspect, with support for Pry end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/results.rb0000644000004100000410000000301114217570270026025 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response # Encapsulates the collection of documents returned from Elasticsearch # # Implements Enumerable and forwards its methods to the {#results} object. # class Results include Base include Enumerable delegate :each, :empty?, :size, :slice, :[], :to_a, :to_ary, to: :results # @see Base#initialize # def initialize(klass, response, options={}) super end # Returns the {Results} collection # def results # TODO: Configurable custom wrapper response.response['hits']['hits'].map { |hit| Result.new(hit) } end alias records results end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response/suggestions.rb0000644000004100000410000000206414217570270026705 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model module Response class Suggestions < HashWrapper disable_warnings if respond_to?(:disable_warnings) def terms self.to_a.map { |k,v| v.first['options'] }.flatten.map {|v| v['text']}.uniq end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/indexing.rb0000644000004100000410000003761114217570270024310 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Provides the necessary support to set up index options (mappings, settings) # as well as instance methods to create, update or delete documents in the index. # # @see ClassMethods#settings # @see ClassMethods#mapping # # @see InstanceMethods#index_document # @see InstanceMethods#update_document # @see InstanceMethods#delete_document # module Indexing # Wraps the [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) # class Settings attr_accessor :settings def initialize(settings={}) @settings = settings end def to_hash @settings end def as_json(options={}) to_hash end end # Wraps the [index mappings](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html) # class Mappings attr_accessor :options, :type # @private TYPES_WITH_EMBEDDED_PROPERTIES = %w(object nested) def initialize(type = nil, options={}) @type = type @options = options @mapping = {} end def indexes(name, options={}, &block) @mapping[name] = options if block_given? @mapping[name][:type] ||= 'object' properties = TYPES_WITH_EMBEDDED_PROPERTIES.include?(@mapping[name][:type].to_s) ? :properties : :fields @mapping[name][properties] ||= {} previous = @mapping begin @mapping = @mapping[name][properties] self.instance_eval(&block) ensure @mapping = previous end end # Set the type to `text` by default @mapping[name][:type] ||= 'text' self end def to_hash if @type { @type.to_sym => @options.merge( properties: @mapping ) } else @options.merge( properties: @mapping ) end end def as_json(options={}) to_hash end end module ClassMethods # Defines mappings for the index # # @example Define mapping for model # # class Article # mapping dynamic: 'strict' do # indexes :foo do # indexes :bar # end # indexes :baz # end # end # # Article.mapping.to_hash # # # => { :article => # # { :dynamic => "strict", # # :properties=> # # { :foo => { # # :type=>"object", # # :properties => { # # :bar => { :type => "string" } # # } # # } # # }, # # :baz => { :type=> "string" } # # } # # } # # @example Define index settings and mappings # # class Article # settings number_of_shards: 1 do # mappings do # indexes :foo # end # end # end # # @example Call the mapping method directly # # Article.mapping(dynamic: 'strict') { indexes :foo, type: 'long' } # # Article.mapping.to_hash # # # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}} # # The `mappings` and `settings` methods are accessible directly on the model class, # when it doesn't already define them. Use the `__elasticsearch__` proxy otherwise. # def mapping(options={}, &block) @mapping ||= Mappings.new(document_type, options) @mapping.options.update(options) unless options.empty? if block_given? @mapping.instance_eval(&block) return self else @mapping end end; alias_method :mappings, :mapping # Define settings for the index # # @example Define index settings # # Article.settings(index: { number_of_shards: 1 }) # # Article.settings.to_hash # # # => {:index=>{:number_of_shards=>1}} # # You can read settings from any object that responds to :read # as long as its return value can be parsed as either YAML or JSON. # # @example Define index settings from YAML file # # # config/elasticsearch/articles.yml: # # # # index: # # number_of_shards: 1 # # # # Article.settings File.open("config/elasticsearch/articles.yml") # # Article.settings.to_hash # # # => { "index" => { "number_of_shards" => 1 } } # # # @example Define index settings from JSON file # # # config/elasticsearch/articles.json: # # # # { "index": { "number_of_shards": 1 } } # # # # Article.settings File.open("config/elasticsearch/articles.json") # # Article.settings.to_hash # # # => { "index" => { "number_of_shards" => 1 } } # def settings(settings={}, &block) settings = YAML.load(settings.read) if settings.respond_to?(:read) @settings ||= Settings.new(settings) @settings.settings.update(settings) unless settings.empty? if block_given? self.instance_eval(&block) return self else @settings end end def load_settings_from_io(settings) YAML.load(settings.read) end # Creates an index with correct name, automatically passing # `settings` and `mappings` defined in the model # # @example Create an index for the `Article` model # # Article.__elasticsearch__.create_index! # # @example Forcefully create (delete first) an index for the `Article` model # # Article.__elasticsearch__.create_index! force: true # # @example Pass a specific index name # # Article.__elasticsearch__.create_index! index: 'my-index' # def create_index!(options={}) options = options.clone target_index = options.delete(:index) || self.index_name settings = options.delete(:settings) || self.settings.to_hash mappings = options.delete(:mappings) || self.mappings.to_hash delete_index!(options.merge index: target_index) if options[:force] unless index_exists?(index: target_index) options.delete(:force) self.client.indices.create({ index: target_index, body: { settings: settings, mappings: mappings } }.merge(options)) end end # Returns true if the index exists # # @example Check whether the model's index exists # # Article.__elasticsearch__.index_exists? # # @example Check whether a specific index exists # # Article.__elasticsearch__.index_exists? index: 'my-index' # def index_exists?(options={}) target_index = options[:index] || self.index_name self.client.indices.exists(index: target_index, ignore: 404) end # Deletes the index with corresponding name # # @example Delete the index for the `Article` model # # Article.__elasticsearch__.delete_index! # # @example Pass a specific index name # # Article.__elasticsearch__.delete_index! index: 'my-index' # def delete_index!(options={}) target_index = options.delete(:index) || self.index_name begin self.client.indices.delete index: target_index rescue Exception => e if e.class.to_s =~ /NotFound/ && options[:force] client.transport.transport.logger.debug("[!!!] Index does not exist (#{e.class})") if client.transport.transport.logger nil else raise e end end end # Performs the "refresh" operation for the index (useful e.g. in tests) # # @example Refresh the index for the `Article` model # # Article.__elasticsearch__.refresh_index! # # @example Pass a specific index name # # Article.__elasticsearch__.refresh_index! index: 'my-index' # # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html # def refresh_index!(options={}) target_index = options.delete(:index) || self.index_name begin self.client.indices.refresh index: target_index rescue Exception => e if e.class.to_s =~ /NotFound/ && options[:force] client.transport.transport.logger.debug("[!!!] Index does not exist (#{e.class})") if client.transport.transport.logger nil else raise e end end end end module InstanceMethods def self.included(base) # Register callback for storing changed attributes for models # which implement `before_save` and return changed attributes # (ie. when `Elasticsearch::Model` is included) # # @note This is typically triggered only when the module would be # included in the model directly, not within the proxy. # # @see #update_document # base.before_save do |obj| if obj.respond_to?(:changes_to_save) # Rails 5.1 changes_to_save = obj.changes_to_save elsif obj.respond_to?(:changes) changes_to_save = obj.changes end if changes_to_save attrs = obj.instance_variable_get(:@__changed_model_attributes) || {} latest_changes = changes_to_save.inject({}) { |latest_changes, (k,v)| latest_changes.merge!(k => v.last) } obj.instance_variable_set(:@__changed_model_attributes, attrs.merge(latest_changes)) end end if base.respond_to?(:before_save) end # Serializes the model instance into JSON (by calling `as_indexed_json`), # and saves the document into the Elasticsearch index. # # @param options [Hash] Optional arguments for passing to the client # # @example Index a record # # @article.__elasticsearch__.index_document # 2013-11-20 16:25:57 +0100: PUT http://localhost:9200/articles/article/1 ... # # @return [Hash] The response from Elasticsearch # # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:index # def index_document(options={}) document = as_indexed_json request = { index: index_name, id: id, body: document } request.merge!(type: document_type) if document_type client.index(request.merge!(options)) end # Deletes the model instance from the index # # @param options [Hash] Optional arguments for passing to the client # # @example Delete a record # # @article.__elasticsearch__.delete_document # 2013-11-20 16:27:00 +0100: DELETE http://localhost:9200/articles/article/1 # # @return [Hash] The response from Elasticsearch # # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete # def delete_document(options={}) request = { index: index_name, id: self.id } request.merge!(type: document_type) if document_type client.delete(request.merge!(options)) end # Tries to gather the changed attributes of a model instance # (via [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)), # performing a _partial_ update of the document. # # When the changed attributes are not available, performs full re-index of the record. # # See the {#update_document_attributes} method for updating specific attributes directly. # # @param options [Hash] Optional arguments for passing to the client # # @example Update a document corresponding to the record # # @article = Article.first # @article.update_attribute :title, 'Updated' # # SQL (0.3ms) UPDATE "articles" SET "title" = ?... # # @article.__elasticsearch__.update_document # # 2013-11-20 17:00:05 +0100: POST http://localhost:9200/articles/article/1/_update ... # # 2013-11-20 17:00:05 +0100: > {"doc":{"title":"Updated"}} # # @return [Hash] The response from Elasticsearch # # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update # def update_document(options={}) if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes).presence attributes = if respond_to?(:as_indexed_json) self.as_indexed_json.select { |k,v| attributes_in_database.keys.map(&:to_s).include? k.to_s } else attributes_in_database end unless attributes.empty? request = { index: index_name, id: self.id, body: { doc: attributes } } request.merge!(type: document_type) if document_type client.update(request.merge!(options)) end else index_document(options) end end # Perform a _partial_ update of specific document attributes # (without consideration for changed attributes as in {#update_document}) # # @param attributes [Hash] Attributes to be updated # @param options [Hash] Optional arguments for passing to the client # # @example Update the `title` attribute # # @article = Article.first # @article.title = "New title" # @article.__elasticsearch__.update_document_attributes title: "New title" # # @return [Hash] The response from Elasticsearch # def update_document_attributes(attributes, options={}) request = { index: index_name, id: self.id, body: { doc: attributes } } request.merge!(type: document_type) if document_type client.update(request.merge!(options)) end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/client.rb0000644000004100000410000000450014217570270023750 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Contains an `Elasticsearch::Client` instance # module Client module ClassMethods # Get the client for a specific model class # # @example Get the client for `Article` and perform API request # # Article.client.cluster.health # # => { "cluster_name" => "elasticsearch" ... } # def client client=nil @client ||= Elasticsearch::Model.client end # Set the client for a specific model class # # @example Configure the client for the `Article` model # # Article.client = Elasticsearch::Client.new host: 'http://api.server:8080' # Article.search ... # def client=(client) @client = client end end module InstanceMethods # Get or set the client for a specific model instance # # @example Get the client for a specific record and perform API request # # @article = Article.first # @article.client.info # # => { "name" => "Node-1", ... } # def client @client ||= self.class.client end # Set the client for a specific model instance # # @example Set the client for a specific record # # @article = Article.first # @article.client = Elasticsearch::Client.new host: 'http://api.server:8080' # def client=(client) @client = client end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/serializing.rb0000644000004100000410000000315314217570270025015 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Contains functionality for serializing model instances for the client # module Serializing module ClassMethods end module InstanceMethods # Serialize the record as a Hash, to be passed to the client. # # Re-define this method to customize the serialization. # # @return [Hash] # # @example Return the model instance as a Hash # # Article.first.__elasticsearch__.as_indexed_json # => {"title"=>"Foo"} # # @see Elasticsearch::Model::Indexing # def as_indexed_json(options={}) # TODO: Play with the `MyModel.indexes` method -- reject non-mapped attributes, `:as` options, etc self.as_json(options.merge root: false) end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/naming.rb0000644000004100000410000001020514217570270023742 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Provides methods for getting and setting index name and document type for the model # module Naming DEFAULT_DOC_TYPE = '_doc'.freeze module ClassMethods # Get or set the name of the index # # @example Set the index name for the `Article` model # # class Article # index_name "articles-#{Rails.env}" # end # # @example Set the index name for the `Article` model and re-evaluate it on each call # # class Article # index_name { "articles-#{Time.now.year}" } # end # # @example Directly set the index name for the `Article` model # # Article.index_name "articles-#{Rails.env}" # # def index_name name=nil, &block if name || block_given? return (@index_name = name || block) end if @index_name.respond_to?(:call) @index_name.call else @index_name || implicit(:index_name) end end # Set the index name # # @see index_name def index_name=(name) @index_name = name end # Get or set the document type # # @example Set the document type for the `Article` model # # class Article # document_type "my-article" # end # # @example Directly set the document type for the `Article` model # # Article.document_type "my-article" # def document_type name=nil @document_type = name || @document_type || implicit(:document_type) end # Set the document type # # @see document_type # def document_type=(name) @document_type = name end private def implicit(prop) self.send("default_#{prop}") end def default_index_name self.model_name.collection.gsub(/\//, '-') end def default_document_type; end end module InstanceMethods # Get or set the index name for the model instance # # @example Set the index name for an instance of the `Article` model # # @article.index_name "articles-#{@article.user_id}" # @article.__elasticsearch__.update_document # def index_name name=nil, &block if name || block_given? return (@index_name = name || block) end if @index_name.respond_to?(:call) @index_name.call else @index_name || self.class.index_name end end # Set the index name # # @see index_name def index_name=(name) @index_name = name end # @example Set the document type for an instance of the `Article` model # # @article.document_type "my-article" # @article.__elasticsearch__.update_document # def document_type name=nil @document_type = name || @document_type || self.class.document_type end # Set the document type # # @see document_type # def document_type=(name) @document_type = name end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/searching.rb0000644000004100000410000001046014217570270024437 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Contains functionality related to searching. # module Searching # Wraps a search request definition # class SearchRequest attr_reader :klass, :definition, :options # @param klass [Class] The class of the model # @param query_or_payload [String,Hash,Object] The search request definition # (string, JSON, Hash, or object responding to `to_hash`) # @param options [Hash] Optional parameters to be passed to the Elasticsearch client # def initialize(klass, query_or_payload, options={}) @klass = klass @options = options __index_name = options[:index] || klass.index_name __document_type = options[:type] || klass.document_type case # search query: ... when query_or_payload.respond_to?(:to_hash) body = query_or_payload.to_hash # search '{ "query" : ... }' when query_or_payload.is_a?(String) && query_or_payload =~ /^\s*{/ body = query_or_payload # search '...' else q = query_or_payload end if body @definition = { index: __index_name, type: __document_type, body: body }.update options else @definition = { index: __index_name, type: __document_type, q: q }.update options end end # Performs the request and returns the response from client # # @return [Hash] The response from Elasticsearch # def execute! klass.client.search(@definition) end end module ClassMethods # Provides a `search` method for the model to easily search within an index/type # corresponding to the model settings. # # @param query_or_payload [String,Hash,Object] The search request definition # (string, JSON, Hash, or object responding to `to_hash`) # @param options [Hash] Optional parameters to be passed to the Elasticsearch client # # @return [Elasticsearch::Model::Response::Response] # # @example Simple search in `Article` # # Article.search 'foo' # # @example Search using a search definition as a Hash # # response = Article.search \ # query: { # match: { # title: 'foo' # } # }, # highlight: { # fields: { # title: {} # } # }, # size: 50 # # response.results.first.title # # => "Foo" # # response.results.first.highlight.title # # => ["Foo"] # # response.records.first.title # # Article Load (0.2ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" IN (1, 3) # # => "Foo" # # @example Search using a search definition as a JSON string # # Article.search '{"query" : { "match_all" : {} }}' # def search(query_or_payload, options={}) search = SearchRequest.new(self, query_or_payload, options) Response::Response.new(self, search) end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/hash_wrapper.rb0000644000004100000410000000226414217570270025162 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Subclass of `Hashie::Mash` to wrap Hash-like structures # (responses from Elasticsearch, search definitions, etc) # # The primary goal of the subclass is to disable the # warning being printed by Hashie for re-defined # methods, such as `sort`. # class HashWrapper < ::Hashie::Mash disable_warnings if respond_to?(:disable_warnings) end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/importing.rb0000644000004100000410000001501614217570270024506 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Provides support for easily and efficiently importing large amounts of # records from the including class into the index. # # @see ClassMethods#import # module Importing # When included in a model, adds the importing methods. # # @example Import all records from the `Article` model # # Article.import # # @see #import # def self.included(base) base.__send__ :extend, ClassMethods adapter = Adapter.from_class(base) base.__send__ :include, adapter.importing_mixin base.__send__ :extend, adapter.importing_mixin end module ClassMethods # Import all model records into the index # # The method will pick up correct strategy based on the `Importing` module # defined in the corresponding adapter. # # @param options [Hash] Options passed to the underlying `__find_in_batches`method # @param block [Proc] Optional block to evaluate for each batch # # @yield [Hash] Gives the Hash with the Elasticsearch response to the block # # @return [Fixnum] default, number of errors encountered during importing # @return [Array] if +return+ option is specified to be +"errors"+, # contains only those failed items in the response +items+ key, e.g.: # # [ # { # "index" => { # "error" => 'FAILED', # "_index" => "test", # "_type" => "_doc", # "_id" => '1', # "_version" => 1, # "result" => "foo", # "_shards" => { # "total" => 1, # "successful" => 0, # "failed" => 1 # }, # "status" => 400 # } # } # ] # # # @example Import all records into the index # # Article.import # # @example Set the batch size to 100 # # Article.import batch_size: 100 # # @example Process the response from Elasticsearch # # Article.import do |response| # puts "Got " + response['items'].select { |i| i['index']['error'] }.size.to_s + " errors" # end # # @example Delete and create the index with appropriate settings and mappings # # Article.import force: true # # @example Refresh the index after importing all batches # # Article.import refresh: true # # @example Import the records into a different index/type than the default one # # Article.import index: 'my-new-index', type: 'my-other-type' # # @example Pass an ActiveRecord scope to limit the imported records # # Article.import scope: 'published' # # @example Pass an ActiveRecord query to limit the imported records # # Article.import query: -> { where(author_id: author_id) } # # @example Transform records during the import with a lambda # # transform = lambda do |a| # {index: {_id: a.id, _parent: a.author_id, data: a.__elasticsearch__.as_indexed_json}} # end # # Article.import transform: transform # # @example Update the batch before yielding it # # class Article # # ... # def self.enrich(batch) # batch.each do |item| # item.metadata = MyAPI.get_metadata(item.id) # end # batch # end # end # # Article.import preprocess: :enrich # # @example Return an array of error elements instead of the number of errors, e.g. to try importing these records again # # Article.import return: 'errors' # def import(options={}, &block) errors = [] refresh = options.delete(:refresh) || false target_index = options.delete(:index) || index_name target_type = options.delete(:type) || document_type transform = options.delete(:transform) || __transform pipeline = options.delete(:pipeline) return_value = options.delete(:return) || 'count' unless transform.respond_to?(:call) raise ArgumentError, "Pass an object responding to `call` as the :transform option, #{transform.class} given" end if options.delete(:force) self.create_index! force: true, index: target_index elsif !self.index_exists? index: target_index raise ArgumentError, "#{target_index} does not exist to be imported into. Use create_index! or the :force option to create it." end __find_in_batches(options) do |batch| params = { index: target_index, type: target_type, body: __batch_to_bulk(batch, transform) } params[:pipeline] = pipeline if pipeline response = client.bulk params yield response if block_given? errors += response['items'].select { |k, v| k.values.first['error'] } end self.refresh_index! index: target_index if refresh case return_value when 'errors' errors else errors.size end end def __batch_to_bulk(batch, transform) batch.map { |model| transform.call(model) } end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/response.rb0000644000004100000410000000536514217570270024342 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Contains modules and classes for wrapping the response from Elasticsearch # module Response # Encapsulate the response returned from the Elasticsearch client # # Implements Enumerable and forwards its methods to the {#results} object. # class Response attr_reader :klass, :search include Enumerable delegate :each, :empty?, :size, :slice, :[], :to_ary, to: :results def initialize(klass, search, options={}) @klass = klass @search = search end # Returns the Elasticsearch response # # @return [Hash] # def response @response ||= HashWrapper.new(search.execute!) end # Returns the collection of "hits" from Elasticsearch # # @return [Results] # def results @results ||= Results.new(klass, self) end # Returns the collection of records from the database # # @return [Records] # def records(options = {}) @records ||= Records.new(klass, self, options) end # Returns the "took" time # def took raw_response['took'] end # Returns whether the response timed out # def timed_out raw_response['timed_out'] end # Returns the statistics on shards # def shards @shards ||= response['_shards'] end # Returns a Hashie::Mash of the aggregations # def aggregations @aggregations ||= Aggregations.new(raw_response['aggregations']) end # Returns a Hashie::Mash of the suggestions # def suggestions @suggestions ||= Suggestions.new(raw_response['suggest']) end def raw_response @raw_response ||= @response ? @response.to_hash : search.execute! end end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/ext/0000755000004100000410000000000014217570270022746 5ustar www-datawww-dataelasticsearch-model-7.2.1/lib/elasticsearch/model/ext/active_record.rb0000644000004100000410000000236114217570270026106 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # Prevent `MyModel.inspect` failing with `ActiveRecord::ConnectionNotEstablished` # (triggered by elasticsearch-model/lib/elasticsearch/model.rb:79:in `included') # ActiveRecord::Base.instance_eval do class << self def inspect_with_rescue inspect_without_rescue rescue ActiveRecord::ConnectionNotEstablished "#{self}(no database connection)" end alias_method_chain :inspect, :rescue end end if defined?(ActiveRecord) && ActiveRecord::VERSION::STRING < '4' elasticsearch-model-7.2.1/lib/elasticsearch/model/multimodel.rb0000644000004100000410000000525414217570270024654 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Keeps a global registry of classes that include `Elasticsearch::Model` # class Registry def initialize @models = [] end # Returns the unique instance of the registry (Singleton) # # @api private # def self.__instance @instance ||= new end # Adds a model to the registry # def self.add(klass) __instance.add(klass) end # Returns an Array of registered models # def self.all __instance.models end # Adds a model to the registry # def add(klass) @models << klass end # Returns a copy of the registered models # def models @models.dup end end # Wraps a collection of models when querying multiple indices # # @see Elasticsearch::Model.search # class Multimodel attr_reader :models # @param models [Class] The list of models across which the search will be performed # def initialize(*models) @models = models.flatten @models = Model::Registry.all if @models.empty? end # Get an Array of index names used for retrieving documents when doing a search across multiple models # # @return [Array] the list of index names used for retrieving documents # def index_name models.map { |m| m.index_name } end # Get an Array of document types used for retrieving documents when doing a search across multiple models # # @return [Array] the list of document types used for retrieving documents # def document_type models.map { |m| m.document_type }.compact.presence end # Get the client common for all models # # @return Elasticsearch::Transport::Client # def client Elasticsearch::Model.client end end end end elasticsearch-model-7.2.1/lib/elasticsearch/model/callbacks.rb0000644000004100000410000000400014217570270024404 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. module Elasticsearch module Model # Allows to automatically update index based on model changes, # by hooking into the model lifecycle. # # @note A blocking HTTP request is done during the update process. # If you need a more performant/resilient way of updating the index, # consider adapting the callbacks behaviour, and use a background # processing solution such as [Sidekiq](http://sidekiq.org) # or [Resque](https://github.com/resque/resque). # module Callbacks # When included in a model, automatically injects the callback subscribers (`after_save`, etc) # # @example Automatically update Elasticsearch index when the model changes # # class Article # include Elasticsearch::Model # include Elasticsearch::Model::Callbacks # end # # Article.first.update_attribute :title, 'Updated' # # SQL (0.3ms) UPDATE "articles" SET "title" = ... # # 2013-11-20 15:08:52 +0100: POST http://localhost:9200/articles/article/1/_update ... # def self.included(base) adapter = Adapter.from_class(base) base.__send__ :include, adapter.callbacks_mixin end end end end elasticsearch-model-7.2.1/elasticsearch-model.gemspec0000644000004100000410000000514614217570270022751 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # coding: utf-8 lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'elasticsearch/model/version' Gem::Specification.new do |s| s.name = 'elasticsearch-model' s.version = Elasticsearch::Model::VERSION s.authors = ['Karel Minarik'] s.email = ['karel.minarik@elasticsearch.org'] s.description = 'ActiveModel/Record integrations for Elasticsearch.' s.summary = 'ActiveModel/Record integrations for Elasticsearch.' s.homepage = 'https://github.com/elasticsearch/elasticsearch-rails/' s.license = 'Apache 2' s.files = `git ls-files`.split($/) s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } s.test_files = s.files.grep(%r{^(test|spec|features)/}) s.require_paths = ['lib'] s.extra_rdoc_files = ['README.md', 'LICENSE.txt'] s.rdoc_options = ['--charset=UTF-8'] s.required_ruby_version = '>= 2.4' s.add_dependency 'activesupport', '> 3' s.add_dependency 'elasticsearch', '~> 7' s.add_dependency 'hashie' s.add_development_dependency 'activemodel', '> 3' s.add_development_dependency 'bundler' s.add_development_dependency 'cane' s.add_development_dependency 'kaminari' s.add_development_dependency 'minitest' s.add_development_dependency 'mocha' s.add_development_dependency 'pry' s.add_development_dependency 'rake', '~> 12' s.add_development_dependency 'require-prof' s.add_development_dependency 'shoulda-context' s.add_development_dependency 'simplecov' s.add_development_dependency 'test-unit' s.add_development_dependency 'turn' s.add_development_dependency 'will_paginate' s.add_development_dependency 'yard' unless defined?(JRUBY_VERSION) s.add_development_dependency 'oj' s.add_development_dependency 'ruby-prof' s.add_development_dependency 'sqlite3' end end elasticsearch-model-7.2.1/Gemfile0000644000004100000410000000166314217570270016767 0ustar www-datawww-data# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. source 'https://rubygems.org' # Specify your gem's dependencies in elasticsearch-model.gemspec gemspec group :development, :testing do gem 'pry-nav' gem 'rspec' end elasticsearch-model-7.2.1/LICENSE.txt0000644000004100000410000002613614217570270017321 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.