elastic_mapper 0.0.1

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