elastic_mapper 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTA5MjVlNTE4OTVmYjg4ZmY5MDc5YzA1NTExZWJhYzY3YmYwZWMzYw==
5
+ data.tar.gz: !binary |-
6
+ YWE3YTVkZjZiNjRhMGNmNTY2ZjdmNDg1OWY4MWZlODI5Y2M4ZjhmNA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NTc2ODlhZmYzYTBhOTIwMGEwZjU2NTU0MGUwMTA0YzU0MTNiNmMyMTI4ZTFk
10
+ OGRjYzBkM2IxYjdiNDBkZDk0ZDNkNzM4NWI0YWY1MmVkMGY1NTU1YTNkMjg5
11
+ ZWY2NjBjMDVjZWI3OGExNmFhNDRiNmM0YWQ3ZGYxNTc1ODRmOGY=
12
+ data.tar.gz: !binary |-
13
+ NzhlNWU0OWM5NDIyNTRhNmQ2YjliZmY2N2FjYjliOTA1ZDI5YmIwMThmYTM3
14
+ MmNmZWUyMzQ4OGM0OTNmOTdkOGRmOGVmMzY4MzgzYjE3ZWZkMzRmZDFlNGJh
15
+ N2YxODVkZGQ2NWZhMjc0MDVhZDJlY2Y0YzMxN2VkOTc2MWY3ZTQ=
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --pattern "**/spec/**/*_spec.rb" --default-path .
2
+ --color
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in elasticmapper.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Benjamin Coe
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ ElasticMapper
2
+ =============
3
+
4
+ A dead simple DSL for integrating ActiveRecord with ElasticSearch.
5
+
6
+ ElasticMapper is built on top of the [Stretcher](https://github.com/PoseBiz/stretcher) library.
7
+
8
+ Background
9
+ ----------
10
+
11
+ Describing Mappings
12
+ ----------------
13
+
14
+ Mappings indicate to ElasticSearch how the fields of a document should be indexed:
15
+
16
+ http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html
17
+
18
+ ElasticMapper provides a `mapping` method, for describing these mappings.
19
+
20
+ ```ruby
21
+ def Article
22
+ include ElasticMapper
23
+
24
+ mapping :title, :doi, { type => :string, index => :not_analyzed }
25
+ mapping :title, :abstract, type => :string
26
+ mapping :publication_date, type => :date
27
+ end
28
+ ```
29
+
30
+ When you first create or modify mappings on an ElasticMapper model, you should run:
31
+
32
+ ```ruby
33
+ Article.put_mapping
34
+ ```
35
+
36
+ ToDo
37
+ ----
38
+
39
+ * Put more tests around search.
40
+ * Test the library out.
41
+
42
+ ## Installation
43
+
44
+ Add this line to your application's Gemfile:
45
+
46
+ gem 'elasticmapper'
47
+
48
+ And then execute:
49
+
50
+ $ bundle
51
+
52
+ Or install it yourself as:
53
+
54
+ $ gem install elasticmapper
55
+
56
+ ## Usage
57
+
58
+ TODO: Write usage instructions here
59
+
60
+ ## Contributing
61
+
62
+ 1. Fork it ( http://github.com/<my-github-username>/elasticmapper/fork )
63
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
64
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
65
+ 4. Push to the branch (`git push origin my-new-feature`)
66
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'elastic_mapper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "elastic_mapper"
8
+ spec.version = ElasticMapper::VERSION
9
+ spec.authors = ["Benjamin Coe"]
10
+ spec.email = ["bencoe@gmail.com"]
11
+ spec.summary = %q{A dead simple DSL for integrating ActiveModel with ElasticSearch.}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "stretcher"
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.5"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "simplecov"
26
+ spec.add_development_dependency "activesupport"
27
+ spec.add_development_dependency "active_hash"
28
+ end
@@ -0,0 +1,23 @@
1
+ # Indexes the ActiveModel instance for search, based on
2
+ # the mapping outlined using ElasticMapper::Mapping.
3
+ module ElasticMapper::Index
4
+
5
+ # Index the ActiveModel in ElasticSearch.
6
+ def index
7
+ mapping_name = self.class.instance_variable_get(:@_mapping_name)
8
+
9
+ ElasticMapper.index.type(mapping_name).put(self.id, index_hash)
10
+ end
11
+
12
+ # Generate a hash representation of the model.
13
+ #
14
+ # @return [Hash] hash representation of model.
15
+ def index_hash
16
+ mapping = self.class.instance_variable_get(:@_mapping)
17
+ mapping.inject({}) do |h, (k, v)|
18
+ h[k] = self.send(v[:field])
19
+ h
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,103 @@
1
+ # Used to describe the mapping for an ActiveModel object,
2
+ # so that it can be indexed for search:
3
+ #
4
+ # On The Topic of Mappings:
5
+ #
6
+ # http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html
7
+ module ElasticMapper::Mapping
8
+
9
+ def self.included(base)
10
+ # Default the mapping name, to the table_name variable.
11
+ # this will be set for ActiveRecord models.
12
+ if base.respond_to?(:table_name)
13
+ base.instance_variable_set(:@_mapping_name, base.table_name.to_sym)
14
+ end
15
+
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ # Populates @_mapping with properties describing
21
+ # how the model should be indexed in ElasticSearch.
22
+ # The last parameter is optionally a hash specifying
23
+ # index settings, e.g., analyzed, not_analyzed.
24
+ #
25
+ # @param args [*args] symbols representing the
26
+ # fields to index.
27
+ def mapping(*args)
28
+ options = {
29
+ :type => :string,
30
+ :index => :analyzed
31
+ }.update(args.extract_options!)
32
+
33
+ args.each do |arg|
34
+ _mapping[mapping_key(arg)] = {
35
+ field: arg,
36
+ options: options
37
+ }
38
+ end
39
+ end
40
+
41
+ # Return the _mapping instance variable, used to keep
42
+ # track of a model's mapping definition. id is added
43
+ # to the model by default, and is used to map the model
44
+ # back onto an ActiveModel object.
45
+ #
46
+ # @return [Hash] the mapping description.
47
+ def _mapping
48
+ @_mapping ||= { id: {
49
+ :field => :id,
50
+ :options => { :type => :integer, :index => :no }
51
+ }}
52
+ end
53
+ private :_mapping
54
+
55
+ # Create a unique key name for the mapping.
56
+ # there are times where you might want to index the
57
+ # same field multiple time, e.g., analyzed, and not_analyzed.
58
+ #
59
+ # @param key [String] the original key name.
60
+ # @return [String] the de-duped key name.
61
+ def mapping_key(key)
62
+ counter = 1
63
+ mapping_key = key
64
+
65
+ while @_mapping.has_key?(mapping_key)
66
+ counter += 1
67
+ mapping_key = "#{key}_#{counter}".to_sym
68
+ end
69
+
70
+ return mapping_key
71
+ end
72
+ private :mapping_key
73
+
74
+ # Override the default name of the mapping.
75
+ #
76
+ # @param mapping_name [String] name of mapping.
77
+ def mapping_name(mapping_name)
78
+ @_mapping_name = mapping_name.to_sym
79
+ end
80
+
81
+ # Generates a hash representation of @_mapping,
82
+ # compatible with ElasticSearch.
83
+ #
84
+ # @return [Hash] mapping.
85
+ def mapping_hash
86
+ {
87
+ @_mapping_name => {
88
+ properties: @_mapping.inject({}) { |h, (k, v)| h[k] = v[:options]; h }
89
+ }
90
+ }
91
+ end
92
+
93
+ # Create the described mapping in ElasticSearch.
94
+ def put_mapping
95
+ ElasticMapper.index
96
+ .type(@_mapping_name)
97
+ .put_mapping(mapping_hash)
98
+
99
+ ElasticMapper.index.refresh
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,123 @@
1
+ # Add search to an ActiveRecord model.
2
+ # The id field is used to load the underlying
3
+ # models from the DB.
4
+ module ElasticMapper::Search
5
+
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Search on a model's mapping. Either a String, or a hash can
13
+ # be provided for the query. If a string is provided,
14
+ # a search will be performed using the ElasticSearch
15
+ # query DSL. If a hash is provided, it will be passed
16
+ # directly to stretcher.
17
+ #
18
+ # @param query [String|Hash] the search query.
19
+ # @param opts [Hash] query options.
20
+ # @return [ActiveModel|Array] the search results.
21
+ def search(query, opts = {}, query_sanitized = false)
22
+
23
+ opts = {
24
+ sort: { _score: 'desc' },
25
+ # from and size, are used to paginate
26
+ # in ElasticSearch
27
+ from: 0,
28
+ size: 20
29
+ }.update(opts)
30
+
31
+ # Convert string query to search DSL.
32
+ query_hash = if query.is_a?(String)
33
+ query_string_to_hash(query)
34
+ else
35
+ query
36
+ end
37
+
38
+ # Perform the query in a try/catch block, attempt
39
+ # to sanitize the query if it fails.
40
+ begin
41
+ res = ElasticMapper.index.type(
42
+ self.instance_variable_get(:@_mapping_name)
43
+ ).search(
44
+ { query: query_hash }.merge(opts)
45
+ )
46
+ rescue Stretcher::RequestError => e
47
+ # the first time a query fails, attempt to
48
+ # sanitize the query and retry the search.
49
+ # This gives users the power of the Lucene DSL
50
+ # while protecting them from badly formed queries.
51
+ if query_sanitized
52
+ raise e
53
+ else
54
+ return search(sanitize_query(query), opts, true)
55
+ end
56
+ end
57
+
58
+ ordered_results(res.results.map(&:id))
59
+ end
60
+
61
+ # Create a query hash from a query string.
62
+ #
63
+ # @param query_string [String] the string query.
64
+ # @return query_hash [Hash] query hash.
65
+ def query_string_to_hash(query_string)
66
+ {
67
+ "bool" => {
68
+ "must" => [
69
+ # This pattern is here for reference, it's useful
70
+ # when it comes time to add multi-tenant support.
71
+ # {"term" => {"user_id" => id}},
72
+ { "query_string" => { "query" => query_string } }
73
+ ]
74
+ }
75
+ }
76
+ end
77
+ private :query_string_to_hash
78
+
79
+ # Fetch a set of ActiveRecord resources, looking up
80
+ # by the id returned by ElasticSearch. Maintain ElasticSearch's
81
+ # ordering.
82
+ #
83
+ # @param ids [Array] array of ordered ids.
84
+ # @return results [Array] ActiveModel result set.
85
+ def ordered_results(ids)
86
+ model_lookup = self.find(ids).inject({}) do |h, m|
87
+ h[m.id] = m
88
+ h
89
+ end
90
+
91
+ ids.map { |id| model_lookup[id] }
92
+ end
93
+ private :ordered_results
94
+
95
+ # sanitize a search query for Lucene. Useful if the original
96
+ # query raises an exception due to invalid DSL parse.
97
+ #
98
+ # http://stackoverflow.com/questions/16205341/symbols-in-query-string-for-elasticsearch
99
+ #
100
+ # @param str [String] the query string to sanitize.
101
+ def sanitize_query(str)
102
+ # Escape special characters
103
+ # http://lucene.apache.org/core/old_versioned_docs/versions/2_9_1/queryparsersyntax.html#Escaping Special Characters
104
+ escaped_characters = Regexp.escape('\\+-&|!(){}[]^~*?:\/')
105
+ str = str.gsub(/([#{escaped_characters}])/, '\\\\\1')
106
+
107
+ # AND, OR and NOT are used by lucene as logical operators. We need
108
+ # to escape them
109
+ ['AND', 'OR', 'NOT'].each do |word|
110
+ escaped_word = word.split('').map {|char| "\\#{char}" }.join('')
111
+ str = str.gsub(/\s*\b(#{word.upcase})\b\s*/, " #{escaped_word} ")
112
+ end
113
+
114
+ # Escape odd quotes
115
+ quote_count = str.count '"'
116
+ str = str.gsub(/(.*)"(.*)/, '\1\"\3') if quote_count % 2 == 1
117
+
118
+ str
119
+ end
120
+ private :sanitize_query
121
+
122
+ end
123
+ end
@@ -0,0 +1,3 @@
1
+ module ElasticMapper
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,58 @@
1
+ require "stretcher"
2
+ require "active_support/core_ext"
3
+ require "elastic_mapper/version"
4
+ require "elastic_mapper/mapping"
5
+ require "elastic_mapper/index"
6
+ require "elastic_mapper/search"
7
+
8
+ module ElasticMapper
9
+
10
+ # The index name to use for ElasticMapper.
11
+ # the models themselves are namespaced
12
+ # by a mapping names.
13
+ #
14
+ # @param index_name [String] name of index.
15
+ def self.index_name=(index_name)
16
+ @@index_name = index_name
17
+ end
18
+
19
+ # Return the index name.
20
+ #
21
+ # @return [String] name of index.
22
+ def self.index_name
23
+ @@index_name
24
+ end
25
+
26
+ # Return the index associated with the
27
+ # default index name.
28
+ #
29
+ # @return [Stretcher::Index] index object.
30
+ def self.index
31
+ ElasticMapper.server.index(index_name)
32
+ end
33
+
34
+ # Allow the ES server to be overriden by an
35
+ # instance with custom initialization.
36
+ #
37
+ # @param server [Stretcher::Server] ES server.
38
+ def self.server=(server)
39
+ @@server = server
40
+ end
41
+
42
+ # Return the server object associated with
43
+ # ElasticMapper.
44
+ #
45
+ # @return [Stretcher::Server]
46
+ def self.server
47
+ @@server ||= Stretcher::Server.new
48
+ end
49
+
50
+ # Include all of the submodules, so that
51
+ # we can optinally use elasticmapper by
52
+ # simply including the root module.
53
+ def self.included(base)
54
+ base.send(:include, ElasticMapper::Mapping)
55
+ base.send(:include, ElasticMapper::Index)
56
+ base.send(:include, ElasticMapper::Search)
57
+ end
58
+ end
@@ -0,0 +1,48 @@
1
+ #encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ElasticMapper::Index do
6
+
7
+ class IndexModel < ActiveHash::Base
8
+ include ElasticMapper
9
+ attr_accessor :foo, :bar
10
+
11
+ mapping :foo, :bar
12
+ mapping :foo, { :type => :string, :index => :not_analyzed }
13
+ mapping_name :index_models
14
+ end
15
+ let(:instance) { IndexModel.create( foo: 'Benjamin', bar: 'Coe' )}
16
+
17
+ describe "index_hash" do
18
+ let(:expected_hash) do
19
+ { :id=>1, :foo=>"Benjamin", :bar=>"Coe", :foo_2=>"Benjamin" }
20
+ end
21
+
22
+ it "creates an index hash that corresponds to the mapping" do
23
+ instance.index_hash.should == expected_hash
24
+ end
25
+ end
26
+
27
+ describe "index" do
28
+
29
+ before(:each) do
30
+ reset_index
31
+ IndexModel.put_mapping
32
+ ElasticMapper.index.refresh
33
+ end
34
+
35
+ it "indexes a document for search" do
36
+ instance.index
37
+ ElasticMapper.index.refresh
38
+
39
+ results = ElasticMapper.index.type(:index_models)
40
+ .search({ size: 12, query: { "query_string" => {"query" => '*'} } })
41
+ .results
42
+
43
+ results.count.should == 1
44
+ results.first.foo.should == 'Benjamin'
45
+ results.first.bar.should == 'Coe'
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,107 @@
1
+ #encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ElasticMapper::Mapping do
6
+
7
+ class Model
8
+ def self.table_name
9
+ :models
10
+ end
11
+
12
+ include ElasticMapper::Mapping
13
+
14
+ mapping :foo, :bar
15
+ mapping :foo, { :type => :string, :index => :not_analyzed }
16
+ end
17
+
18
+ class ModelMappingNameOverridden
19
+ include ElasticMapper::Mapping
20
+
21
+ mapping_name :overridden_name
22
+ end
23
+
24
+ describe "mapping" do
25
+
26
+ let(:mapping) { Model.instance_variable_get(:@_mapping) }
27
+
28
+ it "creates a mapping entry for each symbol" do
29
+ mapping.has_key?(:foo).should == true
30
+ mapping.has_key?(:bar).should == true
31
+ end
32
+
33
+ it "creates unique name for field in mapping, if a collision occurs" do
34
+ mapping.has_key?(:foo_2).should == true
35
+ end
36
+
37
+ it "populates mapping entry with default options, if none given" do
38
+ mapping[:foo][:options].should == {
39
+ :index => :analyzed,
40
+ :type => :string
41
+ }
42
+ end
43
+
44
+ it "allows options to be overridden" do
45
+ mapping[:foo_2][:options].should == {
46
+ :index => :not_analyzed,
47
+ :type => :string
48
+ }
49
+ end
50
+
51
+ end
52
+
53
+ describe "mapping_name" do
54
+ it "defaults to the table_name of the model" do
55
+ Model.instance_variable_get(:@_mapping_name).should == :models
56
+ end
57
+
58
+ it "allows the mapping name to be overridden" do
59
+ ModelMappingNameOverridden
60
+ .instance_variable_get(:@_mapping_name).should == :overridden_name
61
+ end
62
+ end
63
+
64
+ describe "mapping_hash" do
65
+ it "generates the appropriate hash for the mapping" do
66
+ mapping = Model.mapping_hash
67
+ mapping.should == { models: {
68
+ properties: {
69
+ id: { :type => :integer, :index => :no},
70
+ foo: { :type => :string, :index => :analyzed },
71
+ bar: { :type => :string, :index => :analyzed },
72
+ foo_2: { :type => :string, :index => :not_analyzed }
73
+ }
74
+ }
75
+ }
76
+ end
77
+ end
78
+
79
+ describe "put_mapping" do
80
+
81
+ let(:expected_properties) do
82
+ {
83
+ "foo" => { "type" => "string" },
84
+ "foo_2" => { "type" => "string", "index" => "not_analyzed",
85
+ "norms" => { "enabled" => false}, "index_options" => "docs"
86
+ },
87
+ "bar" => { "type" => "string" },
88
+ "id" => { "type" => "integer", "index" => "no" }
89
+ }.stringify_keys
90
+ end
91
+ before(:each) { reset_index }
92
+
93
+ it "creates the mapping in ElasticSearch" do
94
+ Model.put_mapping
95
+
96
+ properties = ElasticMapper.index
97
+ .get_mapping
98
+ .elastic_mapper_tests
99
+ .models
100
+ .properties
101
+ .to_hash
102
+
103
+ properties.should == expected_properties
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,87 @@
1
+ #encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ElasticMapper::Search do
6
+
7
+ class SearchModel < ActiveHash::Base
8
+ include ElasticMapper
9
+ attr_accessor :foo, :bar
10
+
11
+ mapping :foo, :bar
12
+ mapping_name :index_models
13
+ end
14
+
15
+ describe "search" do
16
+ before(:each) do
17
+ reset_index
18
+ IndexModel.put_mapping
19
+ end
20
+ let(:d1) { SearchModel.create(foo: 'hello world', bar: 'goodnight moon') }
21
+ let(:d2) { SearchModel.create(foo: 'alpha century', bar: 'mars') }
22
+ let(:d3) { SearchModel.create(foo: 'cat lover') }
23
+ before(:each) do
24
+ index(d1)
25
+ index(d2)
26
+ index(d3)
27
+ end
28
+
29
+ context "search by query string" do
30
+
31
+ it "returns documents matching a query string" do
32
+ results = SearchModel.search('alpha')
33
+ results.count.should == 1
34
+ results.first.foo.should == 'alpha century'
35
+ end
36
+
37
+ it "supports elasticsearch query DSL" do
38
+ results = SearchModel.search('*')
39
+ results.count.should == 3
40
+ end
41
+
42
+ it "handles escaping invalid search string" do
43
+ results = SearchModel.search('AND AND mars')
44
+ results.count.should == 1
45
+ results.first.foo.should == 'alpha century'
46
+ end
47
+ end
48
+
49
+ context "search by hash" do
50
+ it "returns documents matching the hash query" do
51
+ results = SearchModel.search({ "query_string" => { "query" => 'alpha' } })
52
+ results.count.should == 1
53
+ results.first.foo.should == 'alpha century'
54
+ end
55
+ end
56
+
57
+ context "sort" do
58
+ it "can sort in descending order" do
59
+ results = SearchModel.search('*', sort: { :foo => :desc })
60
+ results.first.foo.should == 'hello world'
61
+ results.second.foo.should == 'cat lover'
62
+ end
63
+
64
+ it "can sort in ascending order" do
65
+ results = SearchModel.search('*', sort: { :foo => :asc })
66
+ results.first.foo.should == 'alpha century'
67
+ results.second.foo.should == 'cat lover'
68
+ end
69
+ end
70
+
71
+ context "pagination" do
72
+ it "allows result size to be set with size" do
73
+ results = SearchModel.search('* OR alpha', size: 1)
74
+ results.count.should == 1
75
+ results.first.foo.should == 'alpha century'
76
+ end
77
+
78
+ it "allows documents to be skipped with from" do
79
+ results = SearchModel.search('* OR alpha', size: 1, from: 1)
80
+ results.count.should == 1
81
+ results.first.foo.should == 'hello world'
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,58 @@
1
+ require "ostruct"
2
+ require "simplecov"
3
+ require "active_hash"
4
+
5
+ SimpleCov.start do
6
+ add_filter "/spec/"
7
+ end
8
+
9
+ SimpleCov.minimum_coverage 95
10
+
11
+ require_relative '../lib/elastic_mapper'
12
+
13
+ # A helper to delete, and recreate the
14
+ # ElasticSearch index used for specs.
15
+ # This code is borrowed from the stretcher specs.
16
+ def reset_index
17
+ ElasticMapper.index_name = "elastic_mapper_tests"
18
+ index = ElasticMapper.index
19
+ server = ElasticMapper.server
20
+
21
+ index.delete rescue nil # ignore exceptions.
22
+
23
+ server.refresh
24
+
25
+ index.create({
26
+ :settings => {
27
+ :number_of_shards => 1,
28
+ :number_of_replicas => 0
29
+ }
30
+ })
31
+
32
+ # Why do both? Doesn't hurt, and it fixes some races
33
+ server.refresh
34
+ index.refresh
35
+
36
+ # Sometimes the index isn't instantly available
37
+ (0..40).each do
38
+ idx_metadata = server.cluster.request(:get, :state)[:metadata][:indices][index.name]
39
+ i_state = idx_metadata[:state]
40
+
41
+ break if i_state == 'open'
42
+
43
+ if attempts_left < 1
44
+ raise "Bad index state! #{i_state}. Metadata: #{idx_metadata}"
45
+ end
46
+
47
+ sleep 0.1
48
+ end
49
+
50
+ end
51
+
52
+ # Index the model provided,
53
+ # and refresh the index so that the
54
+ # document can be searched.
55
+ def index(model)
56
+ model.index
57
+ ElasticMapper.index.refresh
58
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: elastic_mapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Coe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: stretcher
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: active_hash
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description:
112
+ email:
113
+ - bencoe@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - .gitignore
119
+ - .rspec
120
+ - Gemfile
121
+ - LICENSE.txt
122
+ - README.md
123
+ - Rakefile
124
+ - elasticmapper.gemspec
125
+ - lib/elastic_mapper.rb
126
+ - lib/elastic_mapper/index.rb
127
+ - lib/elastic_mapper/mapping.rb
128
+ - lib/elastic_mapper/search.rb
129
+ - lib/elastic_mapper/version.rb
130
+ - spec/elastic_mapper/index_spec.rb
131
+ - spec/elastic_mapper/mapping_spec.rb
132
+ - spec/elastic_mapper/search_spec.rb
133
+ - spec/spec_helper.rb
134
+ homepage: ''
135
+ licenses:
136
+ - MIT
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.2.1
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: A dead simple DSL for integrating ActiveModel with ElasticSearch.
158
+ test_files:
159
+ - spec/elastic_mapper/index_spec.rb
160
+ - spec/elastic_mapper/mapping_spec.rb
161
+ - spec/elastic_mapper/search_spec.rb
162
+ - spec/spec_helper.rb
163
+ has_rdoc: