khoj 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +5 -0
- data/README.md +2 -0
- data/Rakefile +10 -0
- data/khoj.gemspec +23 -0
- data/lib/generators/khoj/khoj_generator.rb +20 -0
- data/lib/generators/khoj/templates/initializer.rb +5 -0
- data/lib/khoj.rb +49 -0
- data/lib/khoj/client.rb +142 -0
- data/lib/khoj/configuration.rb +32 -0
- data/lib/khoj/function.rb +35 -0
- data/lib/khoj/index.rb +28 -0
- data/lib/khoj/version.rb +3 -0
- data/test/client_test.rb +246 -0
- data/test/configuration_test.rb +9 -0
- data/test/facet_test.rb +64 -0
- data/test/function_test.rb +24 -0
- data/test/index_test.rb +68 -0
- data/test/khoj_test.rb +64 -0
- data/test/sort_test.rb +55 -0
- data/test/test_helper.rb +11 -0
- metadata +121 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
data/khoj.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/khoj/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jiren", "Siva", 'Swapnil', 'Pratik']
|
6
|
+
gem.email = ["jiren@joshsoftware.com,siva@joshsoftware.com,
|
7
|
+
swapnil@joshsoftware.com,pratik@joshsoftware.com"]
|
8
|
+
gem.description = %q{Elastic search client}
|
9
|
+
gem.summary = %q{Elastic search client}
|
10
|
+
gem.homepage = ""
|
11
|
+
|
12
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
13
|
+
gem.files = `git ls-files`.split("\n")
|
14
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
gem.name = "khoj"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = Khoj::VERSION
|
18
|
+
gem.add_dependency('json', '>= 1.5.3')
|
19
|
+
gem.add_dependency("httparty", ">= 0.7.8")
|
20
|
+
|
21
|
+
gem.add_development_dependency('activesupport', '>= 3.0.0')
|
22
|
+
gem.add_development_dependency('i18n')
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class KhojGenerator < Rails::Generators::Base
|
2
|
+
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
class_option :api_key, :aliases => '-k', :type => :string, :desc => 'API key.'
|
5
|
+
|
6
|
+
def add_config
|
7
|
+
if options[:api_key]
|
8
|
+
template 'initializer.rb', 'config/initializers/khoj.rb'
|
9
|
+
else
|
10
|
+
p 'Set option --api-key or -k.'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def api_key
|
16
|
+
"'#{options[:api_key]}'"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
data/lib/khoj.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'httparty'
|
3
|
+
|
4
|
+
require 'khoj/version'
|
5
|
+
require 'khoj/configuration'
|
6
|
+
require 'khoj/index'
|
7
|
+
require 'khoj/client'
|
8
|
+
require 'khoj/function'
|
9
|
+
|
10
|
+
module Khoj
|
11
|
+
|
12
|
+
class KhojException < Exception
|
13
|
+
def initialize(message)
|
14
|
+
message = message['error'] if message.class == Hash
|
15
|
+
super "[KHOJ] #{message}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.config(&block)
|
20
|
+
yield Configuration
|
21
|
+
|
22
|
+
unless Configuration.api_host
|
23
|
+
Configuration.api_host = Configuration::DEFAULTS[:api_host]
|
24
|
+
end
|
25
|
+
|
26
|
+
if Configuration.api_key.nil? or Configuration.api_key.strip.empty?
|
27
|
+
Configuration.valid = false
|
28
|
+
raise KhojException.new('api key is nil.')
|
29
|
+
end
|
30
|
+
|
31
|
+
Configuration.valid = true
|
32
|
+
#Configuration.freeze
|
33
|
+
end
|
34
|
+
|
35
|
+
@@clients = {}
|
36
|
+
|
37
|
+
def self.client(index)
|
38
|
+
@@clients[index] ||= Client.new(index)
|
39
|
+
end
|
40
|
+
|
41
|
+
@@functions = {}
|
42
|
+
|
43
|
+
def self.function(index)
|
44
|
+
@@functions[index] ||= Function.new(index)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
end
|
data/lib/khoj/client.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
module Khoj
|
2
|
+
class Client
|
3
|
+
include Index
|
4
|
+
|
5
|
+
DEFAULT_DOC_TYPE = 'default'
|
6
|
+
DEFAULT_DOC_FIELD = 'text'
|
7
|
+
DEFAULT_SEARCH_LIMIT = 10
|
8
|
+
DEFAULT_ORDER = 'asc'
|
9
|
+
|
10
|
+
attr_accessor :index
|
11
|
+
attr_reader :_index
|
12
|
+
|
13
|
+
#alias_method :update_categories, :add
|
14
|
+
|
15
|
+
|
16
|
+
def initialize(index)
|
17
|
+
@index = index
|
18
|
+
@_index = "#{Configuration.api_key}-#{index}"
|
19
|
+
@conn = Configuration.connection
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(resource_id, options = {})
|
23
|
+
response = @conn.get("/#{_index}/#{resource_type(resource_id).join('/')}")
|
24
|
+
response.parsed_response
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(resource_id, options = {})
|
28
|
+
resource_name, id = resource_type(resource_id)
|
29
|
+
resource = if options.class == String
|
30
|
+
return if options.strip.empty?
|
31
|
+
{ DEFAULT_DOC_FIELD => options}
|
32
|
+
else
|
33
|
+
return if options.empty?
|
34
|
+
options
|
35
|
+
end
|
36
|
+
|
37
|
+
req_options = {:body => resource.to_json, :format => 'json'}
|
38
|
+
response = @conn.post("/#{_index}/#{resource_name}/#{id}", req_options)
|
39
|
+
|
40
|
+
case response.code
|
41
|
+
when 200
|
42
|
+
true
|
43
|
+
when 201
|
44
|
+
true
|
45
|
+
else
|
46
|
+
raise KhojException.new(response.parsed_response)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(resource_id)
|
51
|
+
id, resource_name = resource_id.to_s.split(':').reverse
|
52
|
+
resource_type = resource_name == nil ? [id] : [resource_name || DEFAULT_DOC_TYPE, id]
|
53
|
+
response = @conn.delete("/#{_index}/#{resource_type(resource_id).join('/')}")
|
54
|
+
response.code == 200
|
55
|
+
end
|
56
|
+
|
57
|
+
def search(query, options = {})
|
58
|
+
search_uri = options[:type] ? "#{options[:type]}/_search?pretty=true" : '_search?pretty=true'
|
59
|
+
|
60
|
+
# check that if search string contains AND, OR or NOT in query
|
61
|
+
# if it is then we will execute String query of Query DSL
|
62
|
+
# else we will execute normal search query
|
63
|
+
if query.scan(/\sAND|NOT|OR+\s/).empty?
|
64
|
+
|
65
|
+
options[:field] ||= DEFAULT_DOC_FIELD
|
66
|
+
q = {:query =>
|
67
|
+
{:term => { options[:field] => query}}
|
68
|
+
}
|
69
|
+
|
70
|
+
#q[:query][:fields] ||= ['_id' , '_type']
|
71
|
+
q[:size] ||= (options[:size] ? (options[:size] > 10 ? DEFAULT_SEARCH_LIMIT : options[:size]) : DEFAULT_SEARCH_LIMIT) if options[:size]
|
72
|
+
q[:query] = q[:query].merge(:script_fields => { "#{options[:fetch]}" => { :script => "_source.#{options[:fetch]}"}}) if options[:fetch]
|
73
|
+
else
|
74
|
+
|
75
|
+
# TODO : implement functionality for fetch action if specified by user to fetch fields from result set
|
76
|
+
q = get_string_query(query, options)
|
77
|
+
end
|
78
|
+
|
79
|
+
if category_filter = options[:category_filter]
|
80
|
+
q[:facets] = {}
|
81
|
+
q[:filter] = {:term => {}}
|
82
|
+
category_filter.keys.each do |key|
|
83
|
+
q[:facets][key.to_s] = {:terms => {:field => key.to_s}, :facet_filter => {:term => {key => category_filter[key]}}}
|
84
|
+
q[:filter][:term].merge!({key => category_filter[key]})
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
#Check if sorting function is specified in the query.
|
89
|
+
#index.search 'test', :function => {:cordinates => "00,00", :order => 'desc'}
|
90
|
+
# "sort" => {:geo_location => {:cordinates => ["xx,yy"]}, :fields => {:price => 'xxx', :tags => 'xxx'}}
|
91
|
+
if sort = options[:sort]
|
92
|
+
q["sort"] = []
|
93
|
+
sort.keys.each do |key|
|
94
|
+
if key == :geo_location
|
95
|
+
q["sort"] << { "_geo_distance" => { "location" => "#{sort[:geo_location][:cordinates]}", "order" => "#{sort[:geo_location][:order] || DEFAULT_ORDER}", "unit" => "km" }}
|
96
|
+
else
|
97
|
+
q["sort"] << sort[:fields]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
response = @conn.get("/#{_index}/#{search_uri}", :body => q.to_json)
|
103
|
+
|
104
|
+
case response.code
|
105
|
+
when 200
|
106
|
+
response.parsed_response
|
107
|
+
else
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def resource_type(resource_id)
|
114
|
+
id, resource_name = resource_id.to_s.split(':').reverse
|
115
|
+
return [resource_name || DEFAULT_DOC_TYPE, id]
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_string_query(query, options)
|
119
|
+
q = {:query =>
|
120
|
+
{:query_string => {
|
121
|
+
:query => query
|
122
|
+
}
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
# while using string query default field in elastic search is _all
|
127
|
+
# if user specify fields then
|
128
|
+
# if fields contains only one 1 field/word then pass it as 'default_field' parameter
|
129
|
+
# if fields has more than 1 fields/words then pass them as 'fields' parameter
|
130
|
+
fields = options[:fields].split(',').each do |i| i.strip! end if options[:fields]
|
131
|
+
if fields
|
132
|
+
if fields.size == 1
|
133
|
+
q[:query][:query_string] = q[:query][:query_string].merge(:default_field => fields[0])
|
134
|
+
elsif fields.size > 1
|
135
|
+
q[:query][:query_string] = q[:query][:query_string].merge(:fields => fields)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
return q
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Khoj
|
2
|
+
class Configuration
|
3
|
+
DEFAULTS = {
|
4
|
+
:api_host => 'http://localhost:9200'
|
5
|
+
}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :api_key
|
9
|
+
attr_accessor :api_host
|
10
|
+
attr_accessor :valid
|
11
|
+
|
12
|
+
def connection
|
13
|
+
@connection ||= Connection.new(:url => Configuration.api_host).class
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
self.valid
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class Connection
|
23
|
+
include HTTParty
|
24
|
+
format :json
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
self.class.base_uri(options[:url])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Khoj
|
2
|
+
class Function
|
3
|
+
include Index
|
4
|
+
|
5
|
+
attr_accessor :index
|
6
|
+
attr_reader :_index
|
7
|
+
|
8
|
+
def initialize(index)
|
9
|
+
@index = index
|
10
|
+
@_index = "#{Configuration.api_key}-#{index}"
|
11
|
+
@conn = Configuration.connection
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(funtion_name, options ={})
|
15
|
+
type = options[:type]
|
16
|
+
if funtion_name == 'geo_location'
|
17
|
+
geo_mapping = { "#{type}" => { "properties" => { "location" => { "type" => "geo_point" } } } }
|
18
|
+
req_options = {:body => geo_mapping.to_json, :format => 'json'}
|
19
|
+
response = @conn.post("/#{_index}/#{type}/_mapping", req_options)
|
20
|
+
case response.code
|
21
|
+
when 200
|
22
|
+
true
|
23
|
+
when 201
|
24
|
+
true
|
25
|
+
else
|
26
|
+
raise KhojException.new(response.parsed_response)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
raise KhojException.new('No function found with given name')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
data/lib/khoj/index.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Khoj
|
2
|
+
module Index
|
3
|
+
|
4
|
+
def create_index
|
5
|
+
response = @conn.put("/#{_index}")
|
6
|
+
response.code == 200 ? true : (raise KhojException.new(response.parsed_response))
|
7
|
+
end
|
8
|
+
|
9
|
+
def delete_index
|
10
|
+
response = @conn.delete("/#{_index}")
|
11
|
+
response.code == 200 ? true : (raise KhojException.new(response.parsed_response))
|
12
|
+
end
|
13
|
+
|
14
|
+
def index?
|
15
|
+
@conn.head("/#{_index}").code == 200 ? true : false
|
16
|
+
end
|
17
|
+
|
18
|
+
def index_stats
|
19
|
+
response = @conn.get("/#{_index }/_stats")
|
20
|
+
if response.code == 200
|
21
|
+
response.parsed_response['_all']['total']['docs']['count']
|
22
|
+
else
|
23
|
+
raise KhojException.new(response.parsed_response)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/khoj/version.rb
ADDED
data/test/client_test.rb
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ClientTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@api_key = 'test-api-key'
|
6
|
+
@index = 'test-client'
|
7
|
+
|
8
|
+
Khoj.config do |c|
|
9
|
+
c.api_key = @api_key
|
10
|
+
c.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
11
|
+
end
|
12
|
+
|
13
|
+
@client = Khoj.client(@index)
|
14
|
+
@document = {:test => 'from the the test case'}
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
@client.delete('1') rescue ''
|
19
|
+
@client.delete("test") rescue ''
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'should be able to add document' do
|
23
|
+
response = @client.add('test:1', @document)
|
24
|
+
sleep(1)
|
25
|
+
assert_equal true, response
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'should be able to add document to default document type' do
|
29
|
+
response = @client.add('1', @document) # like doc id 'default:1'
|
30
|
+
sleep(1)
|
31
|
+
assert_equal true, response
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'should be able to add document with default text field if string is input' do
|
35
|
+
response = @client.add('1', 'test for default text field')
|
36
|
+
sleep(1)
|
37
|
+
assert_equal true, response
|
38
|
+
end
|
39
|
+
|
40
|
+
test 'should be able to retrive docuemt' do
|
41
|
+
@client.add('test:1', @document)
|
42
|
+
sleep(1)
|
43
|
+
response = @client.get('test:1')
|
44
|
+
|
45
|
+
assert_equal true, response['exists']
|
46
|
+
end
|
47
|
+
|
48
|
+
# default:1 : type => default, id => 1
|
49
|
+
test 'should be able to retrive default type document' do
|
50
|
+
@client.add('1', @document)
|
51
|
+
sleep(1)
|
52
|
+
response = @client.get('1')
|
53
|
+
|
54
|
+
assert_equal true, response['exists']
|
55
|
+
assert_equal Khoj::Client::DEFAULT_DOC_TYPE, response['_type']
|
56
|
+
end
|
57
|
+
|
58
|
+
test 'should be able to delete document' do
|
59
|
+
@client.add('test:1', @document)
|
60
|
+
sleep(1)
|
61
|
+
response = @client.delete('test:1')
|
62
|
+
assert_equal true, response
|
63
|
+
end
|
64
|
+
|
65
|
+
test 'should be able to delete default type document' do
|
66
|
+
@client.add('1', @document)
|
67
|
+
sleep(1)
|
68
|
+
response = @client.delete('1')
|
69
|
+
assert_equal true, response
|
70
|
+
end
|
71
|
+
|
72
|
+
#Default doc type : 'default', Default filed : text
|
73
|
+
test 'should be able to search document using default type and field' do
|
74
|
+
@client.delete(100)
|
75
|
+
@client.add(100, 'xxx yyy zzz')
|
76
|
+
sleep(2)
|
77
|
+
|
78
|
+
response = @client.search('xxx')
|
79
|
+
|
80
|
+
assert_equal 1, response['hits']['total']
|
81
|
+
end
|
82
|
+
|
83
|
+
test 'should be able to search document using field name' do
|
84
|
+
@client.add('test:2', {:test => 'test2'})
|
85
|
+
sleep(1)
|
86
|
+
|
87
|
+
response = @client.search('test2', {:field => 'test'})
|
88
|
+
assert_equal 1, response['hits']['total']
|
89
|
+
end
|
90
|
+
|
91
|
+
test 'should be able to search document by type' do
|
92
|
+
@client.add('test:3', {:test => 'test3'})
|
93
|
+
sleep(1)
|
94
|
+
|
95
|
+
response = @client.search('test3', {:field => 'test', :type => 'test'})
|
96
|
+
assert_equal 1, response['hits']['total']
|
97
|
+
end
|
98
|
+
|
99
|
+
#size
|
100
|
+
test 'should be able to search number of records as size is specified, 10 by default' do
|
101
|
+
15.times do |i|
|
102
|
+
@client.add("test:#{i+1}", 'test application')
|
103
|
+
end
|
104
|
+
sleep(1)
|
105
|
+
response = @client.search('test', :size => 0)
|
106
|
+
assert_equal 0, response['hits']['hits'].count
|
107
|
+
|
108
|
+
response = @client.search('test', :size => 5)
|
109
|
+
assert_equal 5, response['hits']['hits'].count
|
110
|
+
|
111
|
+
response = @client.search('test')
|
112
|
+
assert_equal 10, response['hits']['hits'].count
|
113
|
+
|
114
|
+
response = @client.search('test', :size => 15)
|
115
|
+
assert_equal 10, response['hits']['hits'].count
|
116
|
+
|
117
|
+
6.times do |i|
|
118
|
+
@client.delete("test:#{i+1}") rescue ''
|
119
|
+
end
|
120
|
+
sleep(1)
|
121
|
+
|
122
|
+
response = @client.search('test')
|
123
|
+
assert_equal 9, response['hits']['hits'].count
|
124
|
+
|
125
|
+
response = @client.search('test', :size => 10)
|
126
|
+
assert_equal 9, response['hits']['hits'].count
|
127
|
+
|
128
|
+
15.times do |i|
|
129
|
+
@client.delete("test:#{i+1}") rescue ''
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
test 'should fetch specified field from documents from which search match is found, if not present then should return nil' do
|
134
|
+
field = 'designation'
|
135
|
+
designation = 'Software Engineer'
|
136
|
+
|
137
|
+
# matching text 'software' with designation
|
138
|
+
@client.add('test:1', :text => 'I am a software engineer', :city => 'Pune', :designation => designation)
|
139
|
+
|
140
|
+
# matching text without designation
|
141
|
+
@client.add('test:2', :text => 'I am a freelancer who developes software', :city => 'Mumbai')
|
142
|
+
|
143
|
+
#unmatching text with designation
|
144
|
+
@client.add('test:3', :text => 'I do marketing of products', :city => 'Delhi', :designation => 'Marketing Executive')
|
145
|
+
sleep(1)
|
146
|
+
|
147
|
+
response = @client.search('software', :fetch=> field)
|
148
|
+
|
149
|
+
ids = [] # ids array will contain ids of documents
|
150
|
+
fields = [] # fields array will contain fetched fields, each field is in key => value format such as "fields"=>{"designation"=>"Software Engineer"}
|
151
|
+
field_values = [] # fields_values will contain values of fetched fields such as 'Software Engineer','Marketing Executive',nil etc.
|
152
|
+
|
153
|
+
# Data received in json format is,
|
154
|
+
# {
|
155
|
+
# "took"=>5, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0},
|
156
|
+
# "hits"=>{"total"=>2, "max_score"=>0.15342641,
|
157
|
+
# "hits"=>[
|
158
|
+
# {"_index"=>"test-api-key-test", "_type"=>"test", "_id"=>"1", "_score"=>0.15342641, "fields"=>{"designation"=>"Software Engineer"}},
|
159
|
+
# {"_index"=>"test-api-key-test", "_type"=>"test", "_id"=>"2", "_score"=>0.11506981, "fields"=>{"designation"=>nil}}
|
160
|
+
# ]
|
161
|
+
# }
|
162
|
+
# }
|
163
|
+
|
164
|
+
response['hits']['hits'].each do |i|
|
165
|
+
ids << i['_id']
|
166
|
+
fields << i['fields']
|
167
|
+
field_values << i['fields'].values
|
168
|
+
end
|
169
|
+
field_values.flatten!
|
170
|
+
# as per data entered, hits should be 2 as word 'software' is in only 2 documents
|
171
|
+
assert_equal 2, response['hits']['total']
|
172
|
+
|
173
|
+
# fields array should contain uniq element as only one field is fetched
|
174
|
+
assert_equal 1, fields.collect(&:keys).flatten.uniq.count
|
175
|
+
|
176
|
+
# key of every element in fields array should be 'designation'
|
177
|
+
assert_equal field, fields.collect(&:keys).flatten.uniq.first
|
178
|
+
|
179
|
+
# ids array should contain 1 and 2, not 3
|
180
|
+
assert_equal true, ids.include?('1')
|
181
|
+
assert_equal true, ids.include?('2')
|
182
|
+
assert_equal false, ids.include?('3')
|
183
|
+
|
184
|
+
# Designation value should be
|
185
|
+
# software enginner for document test:1 and nil for test:2
|
186
|
+
# it should not contain value as Marketing Executive
|
187
|
+
assert_equal true, field_values.include?(designation)
|
188
|
+
assert_equal true, field_values.include?(nil)
|
189
|
+
assert_equal false, field_values.include?('Marketing Executive')
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
test 'should perofrm search operation using operators[AND/OR/NOT], when no field specified, default is all fields' do
|
194
|
+
add_data_with_multiple_fields
|
195
|
+
|
196
|
+
# Search data with operators using no fields specified
|
197
|
+
response = @client.search('marketing OR tester')
|
198
|
+
ids = get_ids_from_response(response)
|
199
|
+
assert_equal 2, ids.size
|
200
|
+
assert_equal true, ids.include?('3')
|
201
|
+
assert_equal true, ids.include?('4')
|
202
|
+
end
|
203
|
+
|
204
|
+
test 'should perofrm operations using operators, when single field is specified' do
|
205
|
+
add_data_with_multiple_fields
|
206
|
+
# Search data with operators and within single field
|
207
|
+
response = @client.search('delhi OR mumbai', :fields => "city")
|
208
|
+
ids = get_ids_from_response(response)
|
209
|
+
assert_equal 2, response['hits']['total']
|
210
|
+
assert_equal true, ids.include?('2')
|
211
|
+
assert_equal true, ids.include?('3')
|
212
|
+
|
213
|
+
response = @client.search('tester OR pune', :fields => "text")
|
214
|
+
assert_equal 0, response['hits']['total']
|
215
|
+
end
|
216
|
+
|
217
|
+
test 'should perofrm operations using operators, when multiple fields are specified' do
|
218
|
+
add_data_with_multiple_fields
|
219
|
+
# Search data with operators and with multiple fields
|
220
|
+
response = @client.search('software AND mumbai', :fields => "designation, city")
|
221
|
+
assert_equal 1, response['hits']['total']
|
222
|
+
assert_equal '2', response['hits']['hits'].first['_id']
|
223
|
+
|
224
|
+
response = @client.search('software OR pune NOT tester NOT manager NOT mumbai', :fields => "designation, city")
|
225
|
+
assert_equal 1, response['hits']['total']
|
226
|
+
assert_equal '1', response['hits']['hits'].first['_id']
|
227
|
+
end
|
228
|
+
|
229
|
+
def add_data_with_multiple_fields
|
230
|
+
# While changig data in this method make sure to do changed in all respective places as the data fields are used for validation of test cases
|
231
|
+
@client.add('test:1', :text => 'I am a software engineer', :city => 'Pune', :designation => 'Software engineer')
|
232
|
+
@client.add('test:2', :text => 'I am a freelancer who developes software', :city => 'Mumbai', :designation => 'Senior Software Engineer')
|
233
|
+
@client.add('test:3', :text => 'I do marketing of products', :city => 'Delhi', :designation => 'Marketing Executive')
|
234
|
+
@client.add('test:4', :text => 'I test the product', :city => 'Pune, University Road', :designation => 'Tester')
|
235
|
+
@client.add('test:5', :text => 'I manage the company', :city => 'Pune, Shivajinagar', :designation => 'Manager')
|
236
|
+
sleep(1)
|
237
|
+
end
|
238
|
+
|
239
|
+
def get_ids_from_response(response)
|
240
|
+
ids = []
|
241
|
+
response['hits']['hits'].each do |i|
|
242
|
+
ids << i['_id']
|
243
|
+
end
|
244
|
+
return ids
|
245
|
+
end
|
246
|
+
end
|
data/test/facet_test.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FacetTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@api_key = 'test-api-key'
|
6
|
+
@index = 'test-facet'
|
7
|
+
|
8
|
+
Khoj.config do |c|
|
9
|
+
c.api_key = @api_key
|
10
|
+
c.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
11
|
+
end
|
12
|
+
|
13
|
+
@client = Khoj.client(@index)
|
14
|
+
@document = {:test => 'from the the test case'}
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
@client.delete('test:1') rescue ''
|
19
|
+
@client.delete('test:2') rescue ''
|
20
|
+
@client.delete('test:3') rescue ''
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'should be able to filter facet count' do
|
24
|
+
@client.add('test:1', {:test => 'test1', :tags => ['foo']})
|
25
|
+
@client.add('test:2', {:test => 'test2', :tags => ['baz']})
|
26
|
+
sleep(5)
|
27
|
+
response = @client.search('test1', {:field => 'test', :type => 'test', :category_filter =>{'tags' => ['foo']}})
|
28
|
+
assert_equal 1, response['facets']['tags']['total']
|
29
|
+
assert_equal 1, response['hits']['total']
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'facet count depends on search result' do
|
33
|
+
@client.add('test:1', {:test => 'test1', :tags => ['foo']})
|
34
|
+
@client.add('test:2', {:test => 'test2', :tags => ['foo']})
|
35
|
+
sleep(5)
|
36
|
+
|
37
|
+
response = @client.search('test1', {:field => 'test', :type => 'test', :category_filter =>{'tags' => ['foo']}})
|
38
|
+
|
39
|
+
assert_equal 1, response['facets']['tags']['total']
|
40
|
+
assert_equal 1, response['hits']['total']
|
41
|
+
end
|
42
|
+
|
43
|
+
test 'should be able to filter multiple facet count' do
|
44
|
+
@client.add('test:3', {:test => 'test3', :tags => ['foo'], :location => 'london'})
|
45
|
+
sleep(5)
|
46
|
+
|
47
|
+
response = @client.search('test3', {:field => 'test', :type => 'test', :category_filter =>{'tags' => ['foo'], 'location' => 'london'}})
|
48
|
+
|
49
|
+
assert_equal 1, response['facets']['tags']['total']
|
50
|
+
assert_equal 1, response['facets']['location']['total']
|
51
|
+
assert_equal 1, response['hits']['total']
|
52
|
+
end
|
53
|
+
|
54
|
+
test 'should perform AND operation on multiple value passed for single field' do
|
55
|
+
@client.add('test:1', {:test => 'test1', :tags => ['foo'], :location => ['pune', 'mumbai', 'delhi']})
|
56
|
+
@client.add('test:2', {:test => 'test2', :tags => ['foo'], :location => ['pune', 'mumbai']})
|
57
|
+
sleep(5)
|
58
|
+
|
59
|
+
response = @client.search('test1', {:field => 'test', :type => 'test', :category_filter =>{'location' => ['pune', 'mumbai', 'delhi']}})
|
60
|
+
assert_equal 3, response['facets']['location']['total']
|
61
|
+
assert_equal 1, response['hits']['total']
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FunctionTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@api_key = 'test-api-key'
|
7
|
+
@index = 'test'
|
8
|
+
|
9
|
+
Khoj.config do |c|
|
10
|
+
c.api_key = @api_key
|
11
|
+
c.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
12
|
+
end
|
13
|
+
|
14
|
+
@client= Khoj.client(@index)
|
15
|
+
@function = Khoj.function(@index)
|
16
|
+
end
|
17
|
+
|
18
|
+
test 'should be able to map document field type to geo_type' do
|
19
|
+
response = @function.add('geo_location', :type => 'function_test')
|
20
|
+
sleep(1)
|
21
|
+
assert_equal true, response
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/test/index_test.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class IndexTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@api_key = 'test-api-key'
|
6
|
+
@index = 'test'
|
7
|
+
|
8
|
+
Khoj.config do |c|
|
9
|
+
c.api_key = @api_key
|
10
|
+
c.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
begin
|
16
|
+
client = Khoj.client(@index)
|
17
|
+
client.delete_index
|
18
|
+
rescue Exception => e
|
19
|
+
""
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'should create index. check index exist or not and delete index' do
|
24
|
+
@index = "test_1"
|
25
|
+
client = Khoj.client(@index)
|
26
|
+
|
27
|
+
assert_equal true, client.create_index
|
28
|
+
assert_equal true, client.index?
|
29
|
+
assert_equal true, client.delete_index
|
30
|
+
end
|
31
|
+
|
32
|
+
test 'should throw exception on create if index already exist' do
|
33
|
+
@index = "test_2"
|
34
|
+
client = Khoj.client(@index)
|
35
|
+
client.create_index
|
36
|
+
|
37
|
+
assert_raise Khoj::KhojException do
|
38
|
+
client.create_index
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
test 'should throw exception on delete if index not exist' do
|
43
|
+
@index = "test_3"
|
44
|
+
client = Khoj.client(@index)
|
45
|
+
|
46
|
+
assert_raise Khoj::KhojException do
|
47
|
+
client.delete_index
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
test 'should return index state' do
|
52
|
+
@index = "test_4"
|
53
|
+
client = Khoj.client(@index)
|
54
|
+
client.create_index
|
55
|
+
|
56
|
+
assert_equal 0, client.index_stats
|
57
|
+
end
|
58
|
+
|
59
|
+
test 'index name should be in lower case' do
|
60
|
+
@index = "TEST-INDEX"
|
61
|
+
client = Khoj.client(@index)
|
62
|
+
|
63
|
+
assert_raise Khoj::KhojException do
|
64
|
+
client.create_index
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
data/test/khoj_test.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
|
4
|
+
class KhojTest < ActiveSupport::TestCase
|
5
|
+
def setup
|
6
|
+
@api_key = 'API-KEY'
|
7
|
+
@api_host = 'http://testclient.com'
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
end
|
12
|
+
|
13
|
+
test 'should init configuration' do
|
14
|
+
Khoj.config do |c|
|
15
|
+
c.api_key = @api_key
|
16
|
+
end
|
17
|
+
|
18
|
+
assert_equal @api_key, Khoj::Configuration.api_key
|
19
|
+
assert_equal Khoj::Configuration::DEFAULTS[:api_host], Khoj::Configuration.api_host
|
20
|
+
assert_equal true, Khoj::Configuration.valid?
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'for nil or empty api key config should not be valid' do
|
24
|
+
assert_raise Khoj::KhojException do
|
25
|
+
Khoj.config do |c|
|
26
|
+
c.api_key = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
assert_raise Khoj::KhojException do
|
31
|
+
Khoj.config do |c|
|
32
|
+
c.api_key = " "
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
test 'should set api host' do
|
38
|
+
Khoj.config do |c|
|
39
|
+
c.api_key = @api_key
|
40
|
+
c.api_host = @api_host
|
41
|
+
end
|
42
|
+
|
43
|
+
assert_equal @api_host, Khoj::Configuration.api_host
|
44
|
+
|
45
|
+
Khoj::Configuration.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
test 'should create api client' do
|
50
|
+
api_key = 'test-api-key'
|
51
|
+
index = 'test'
|
52
|
+
Khoj.config do |c|
|
53
|
+
c.api_key = api_key
|
54
|
+
c.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
55
|
+
end
|
56
|
+
|
57
|
+
client = Khoj.client(index)
|
58
|
+
|
59
|
+
assert_equal 'test', client.index
|
60
|
+
assert_equal "#{api_key}-#{index}", client._index
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
end
|
data/test/sort_test.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SortTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@api_key = 'test-api-key-new'
|
7
|
+
@index = 'test-new'
|
8
|
+
|
9
|
+
Khoj.config do |c|
|
10
|
+
c.api_key = @api_key
|
11
|
+
c.api_host = Khoj::Configuration::DEFAULTS[:api_host]
|
12
|
+
end
|
13
|
+
|
14
|
+
@client= Khoj.client(@index)
|
15
|
+
@function = Khoj.function(@index)
|
16
|
+
@function.add('geo_location', :type => 'sort_test')
|
17
|
+
@client.add('sort_test:1', :text => 'sort_test', :location => {:lat => '100.00', :lon => "100.00"}, :price => '100' ,:range => '200')
|
18
|
+
@client.add('sort_test:2', :text => 'sort_test', :location => {:lat => '50.00', :lon => "50.00"}, :price => '200', :range => '100')
|
19
|
+
@client.add('sort_test:3', :text => 'sort_test', :location => {:lat => '00.00', :lon => "00.00"}, :price => '300', :range => '100')
|
20
|
+
sleep(5)
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'should be able to sort document according to location in ascending order' do
|
28
|
+
response = @client.search 'sort_test', :sort => {:geo_location => {:cordinates => "100,100", :order => 'asc'}}, :size => 3, :type => 'sort_test'
|
29
|
+
assert_equal "1", response["hits"]["hits"][0]["_id"]
|
30
|
+
assert_equal "3", response["hits"]["hits"][1]["_id"]
|
31
|
+
assert_equal "2", response["hits"]["hits"][2]["_id"]
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'should be able to sort document according to location in descending order' do
|
35
|
+
response = @client.search 'sort_test', :sort => {:geo_location => {:cordinates => "100,100", :order => 'desc'}}, :size => 3, :type => 'sort_test'
|
36
|
+
assert_equal "2", response["hits"]["hits"][0]["_id"]
|
37
|
+
assert_equal "3", response["hits"]["hits"][1]["_id"]
|
38
|
+
assert_equal "1", response["hits"]["hits"][2]["_id"]
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'should be able to sort document according price range' do
|
42
|
+
response = @client.search 'sort_test', :sort => {:fields => {:price => "asc"}}, :size => 3, :type => 'sort_test'
|
43
|
+
assert_equal "1", response["hits"]["hits"][0]["_id"]
|
44
|
+
assert_equal "2", response["hits"]["hits"][1]["_id"]
|
45
|
+
assert_equal "3", response["hits"]["hits"][2]["_id"]
|
46
|
+
end
|
47
|
+
|
48
|
+
test 'should be able to sort document primarily by range and then price range' do
|
49
|
+
response = @client.search 'sort_test', :sort => {:fields => {:range => 'desc', :price => "desc"}}, :size => 3, :type => 'sort_test'
|
50
|
+
assert_equal "1", response["hits"]["hits"][0]["_id"]
|
51
|
+
assert_equal "3", response["hits"]["hits"][1]["_id"]
|
52
|
+
assert_equal "2", response["hits"]["hits"][2]["_id"]
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'active_support/test_case'
|
3
|
+
|
4
|
+
unless $LOAD_PATH.include? 'lib'
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join($LOAD_PATH.first, '..', 'lib'))
|
7
|
+
end
|
8
|
+
|
9
|
+
#NOTE: rake test TEST=test.rb
|
10
|
+
|
11
|
+
require 'khoj'
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: khoj
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jiren
|
9
|
+
- Siva
|
10
|
+
- Swapnil
|
11
|
+
- Pratik
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
date: 2012-01-11 00:00:00.000000000 Z
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: json
|
19
|
+
requirement: &2162535300 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ! '>='
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 1.5.3
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: *2162535300
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: httparty
|
30
|
+
requirement: &2162534800 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.7.8
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: *2162534800
|
39
|
+
- !ruby/object:Gem::Dependency
|
40
|
+
name: activesupport
|
41
|
+
requirement: &2162534340 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.0.0
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: *2162534340
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: i18n
|
52
|
+
requirement: &2162533960 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
type: :development
|
59
|
+
prerelease: false
|
60
|
+
version_requirements: *2162533960
|
61
|
+
description: Elastic search client
|
62
|
+
email:
|
63
|
+
- ! "jiren@joshsoftware.com,siva@joshsoftware.com,\n swapnil@joshsoftware.com,pratik@joshsoftware.com"
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- .gitignore
|
69
|
+
- Gemfile
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- khoj.gemspec
|
73
|
+
- lib/generators/khoj/khoj_generator.rb
|
74
|
+
- lib/generators/khoj/templates/initializer.rb
|
75
|
+
- lib/khoj.rb
|
76
|
+
- lib/khoj/client.rb
|
77
|
+
- lib/khoj/configuration.rb
|
78
|
+
- lib/khoj/function.rb
|
79
|
+
- lib/khoj/index.rb
|
80
|
+
- lib/khoj/version.rb
|
81
|
+
- test/client_test.rb
|
82
|
+
- test/configuration_test.rb
|
83
|
+
- test/facet_test.rb
|
84
|
+
- test/function_test.rb
|
85
|
+
- test/index_test.rb
|
86
|
+
- test/khoj_test.rb
|
87
|
+
- test/sort_test.rb
|
88
|
+
- test/test_helper.rb
|
89
|
+
homepage: ''
|
90
|
+
licenses: []
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.8.10
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Elastic search client
|
113
|
+
test_files:
|
114
|
+
- test/client_test.rb
|
115
|
+
- test/configuration_test.rb
|
116
|
+
- test/facet_test.rb
|
117
|
+
- test/function_test.rb
|
118
|
+
- test/index_test.rb
|
119
|
+
- test/khoj_test.rb
|
120
|
+
- test/sort_test.rb
|
121
|
+
- test/test_helper.rb
|