Skip to content

Commit 8ad39cf

Browse files
committed
Integration tests for Elasticsearch query generation
Add a few tests that exercise the full query parse-generation-execution cycle to validate the generated queries. Includes some helpers for inserting data into Elasticsearch in order to have something to search over. The index is re-created and the data re-indexed at the beginning of the test suite run. To run these tests, Elasticsearch must be running.
1 parent 8b4cfb3 commit 8ad39cf

File tree

6 files changed

+153
-7
lines changed

6 files changed

+153
-7
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ gem 'parslet'
55
gem 'minitest'
66
gem 'awesome_print'
77
gem 'pry'
8+
gem 'elasticsearch'

Gemfile.lock

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@ GEM
44
awesome_print (1.7.0)
55
blankslate (3.1.3)
66
coderay (1.1.1)
7+
elasticsearch (5.0.4)
8+
elasticsearch-api (= 5.0.4)
9+
elasticsearch-transport (= 5.0.4)
10+
elasticsearch-api (5.0.4)
11+
multi_json
12+
elasticsearch-transport (5.0.4)
13+
faraday
14+
multi_json
15+
faraday (0.12.0.1)
16+
multipart-post (>= 1.2, < 3)
717
method_source (0.8.2)
818
minitest (5.10.1)
19+
multi_json (1.12.1)
20+
multipart-post (2.0.0)
921
parslet (1.7.1)
1022
blankslate (>= 2.0, <= 4.0)
1123
pry (0.10.4)
@@ -19,6 +31,7 @@ PLATFORMS
1931

2032
DEPENDENCIES
2133
awesome_print
34+
elasticsearch
2235
minitest
2336
parslet
2437
pry

boolean_term_parser.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ def to_elasticsearch
4343
}
4444

4545
if should_terms.any?
46-
query[:query][:boolean][:should] = should_terms.map { |term| match(term) }
46+
query[:query][:bool][:should] = should_terms.map { |term| match(term) }
4747
end
4848

4949
if must_terms.any?
50-
query[:query][:boolean][:must] = must_terms.map { |term| match(term) }
50+
query[:query][:bool][:must] = must_terms.map { |term| match(term) }
5151
end
5252

5353
if must_not_terms.any?
54-
query[:query][:boolean][:must_not] = must_not_terms.map { |term| match(term) }
54+
query[:query][:bool][:must_not] = must_not_terms.map { |term| match(term) }
5555
end
5656

5757
query

elasticsearch_helpers.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
require 'elasticsearch'
2+
3+
module ElasticsearchHelpers
4+
def self.client
5+
@client ||= Elasticsearch::Client.new(:log => true)
6+
end
7+
8+
def self.search(query_dsl)
9+
client.search(:index => 'query_parser_test', :type => 'books', :body => query_dsl)
10+
end
11+
12+
def self.create_index!
13+
if client.indices.exists?(:index => 'query_parser_test')
14+
client.indices.delete(:index => 'query_parser_test')
15+
end
16+
17+
client.indices.create(:index => 'query_parser_test',
18+
:body => {
19+
:settings => {
20+
:index => {
21+
:mapper => {
22+
:dynamic => false
23+
}
24+
}
25+
},
26+
:mappings => {
27+
:books => {
28+
:_all => { :enabled => false },
29+
:properties => {
30+
:title => {
31+
:type => 'text',
32+
:analyzer => 'standard'
33+
},
34+
:author => {
35+
:type => 'text',
36+
:analyzer => 'standard'
37+
},
38+
:publication_year => {
39+
:type => 'integer'
40+
}
41+
}
42+
}
43+
}
44+
})
45+
end
46+
47+
def self.prepare_corpus!
48+
create_index!
49+
index_documents
50+
wait_for_indexing
51+
end
52+
53+
# It takes a moment for documents to become available for search
54+
def self.wait_for_indexing
55+
retries = 0
56+
loop do
57+
results = search(:query => {:match_all => {}})
58+
59+
if results['hits']['hits'].size == corpus.size
60+
break
61+
end
62+
63+
if retries > 5
64+
raise "Error indexing corpus. Got these results: #{results}"
65+
end
66+
67+
retries += 1
68+
sleep(0.5)
69+
end
70+
end
71+
72+
def self.corpus
73+
[
74+
{:title => "The Cat in the Hat", :author => ["Theodor Geisel", "Doctor Seuss"], :publication_year => 1957},
75+
{:title => "Cat Sense", :author => "John Bradshaw", :publication_year => 2013},
76+
{:title => "How to Tell If Your Cat Is Plotting to Kill You", :author => "Matthew Inman", :publication_year => 2012}
77+
]
78+
end
79+
80+
def self.index_documents
81+
corpus.each_with_index do |doc, index|
82+
client.index(:index => 'query_parser_test',
83+
:type => 'books',
84+
:id => index,
85+
:body => doc)
86+
87+
end
88+
end
89+
end

phrase_parser.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ def initialize(operator, phrase)
4444
class PhraseQuery
4545
attr_accessor :should_clauses, :must_not_clauses, :must_clauses
4646

47+
def self.elasticsearch_query_for(query_string)
48+
tree = PhraseParser.new.parse(query_string)
49+
query = PhraseTransformer.new.apply(tree)
50+
query.to_elasticsearch
51+
end
52+
4753
def initialize(clauses)
4854
self.should_clauses = clauses.select { |c| c.operator == :should }
4955
self.must_not_clauses = clauses.select { |c| c.operator == :must_not }
@@ -53,21 +59,21 @@ def initialize(clauses)
5359
def to_elasticsearch
5460
query = {
5561
:query => {
56-
:boolean => {
62+
:bool => {
5763
}
5864
}
5965
}
6066

6167
if should_clauses.any?
62-
query[:query][:boolean][:should] = should_clauses.map { |clause| clause_to_query(clause) }
68+
query[:query][:bool][:should] = should_clauses.map { |clause| clause_to_query(clause) }
6369
end
6470

6571
if must_clauses.any?
66-
query[:query][:boolean][:must] = must_clauses.map { |clause| clause_to_query(clause) }
72+
query[:query][:bool][:must] = must_clauses.map { |clause| clause_to_query(clause) }
6773
end
6874

6975
if must_not_clauses.any?
70-
query[:query][:boolean][:must_not] = must_not_clauses.map { |clause| clause_to_query(clause) }
76+
query[:query][:bool][:must_not] = must_not_clauses.map { |clause| clause_to_query(clause) }
7177
end
7278

7379
query

tests/integration/search_test.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
require 'minitest/autorun'
2+
require_relative '../../phrase_parser'
3+
require_relative '../../elasticsearch_helpers'
4+
5+
class SearchTest < Minitest::Test
6+
7+
def self.configure_es
8+
return if defined?(@configured)
9+
10+
ElasticsearchHelpers.prepare_corpus!
11+
12+
@configured = true
13+
end
14+
15+
def setup
16+
self.class.configure_es
17+
end
18+
19+
def test_query
20+
query_dsl = PhraseQuery.elasticsearch_query_for('kill "cat is plotting"')
21+
results = ElasticsearchHelpers.search(query_dsl)
22+
23+
hits = results['hits']['hits']
24+
assert_equal(1, hits.size)
25+
assert_equal('How to Tell If Your Cat Is Plotting to Kill You', hits.first['_source']['title'])
26+
end
27+
28+
def test_negation_query
29+
query_dsl = PhraseQuery.elasticsearch_query_for('cat -hat')
30+
results = ElasticsearchHelpers.search(query_dsl)
31+
32+
hits = results['hits']['hits']
33+
assert_equal(2, hits.size)
34+
titles = hits.map { |h| h['_source']['title'] }
35+
refute_includes(titles, "The Cat in the Hat")
36+
end
37+
end

0 commit comments

Comments
 (0)