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 +15 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +66 -0
- data/Rakefile +4 -0
- data/elasticmapper.gemspec +28 -0
- data/lib/elastic_mapper/index.rb +23 -0
- data/lib/elastic_mapper/mapping.rb +103 -0
- data/lib/elastic_mapper/search.rb +123 -0
- data/lib/elastic_mapper/version.rb +3 -0
- data/lib/elastic_mapper.rb +58 -0
- data/spec/elastic_mapper/index_spec.rb +48 -0
- data/spec/elastic_mapper/mapping_spec.rb +107 -0
- data/spec/elastic_mapper/search_spec.rb +87 -0
- data/spec/spec_helper.rb +58 -0
- metadata +163 -0
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
data/.rspec
ADDED
data/Gemfile
ADDED
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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|