icfs 0.1.0
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 +7 -0
- data/LICENSE.txt +674 -0
- data/bin/icfs_demo_create.rb +89 -0
- data/bin/icfs_demo_fcgi.rb +51 -0
- data/bin/icfs_demo_ssl_gen.rb +84 -0
- data/bin/icfs_demo_web.rb +50 -0
- data/bin/icfs_dev_todo.rb +20 -0
- data/data/demo_config.yml +94 -0
- data/data/icfs.css +475 -0
- data/data/icfs.js +458 -0
- data/lib/icfs.rb +109 -0
- data/lib/icfs/api.rb +1436 -0
- data/lib/icfs/cache.rb +254 -0
- data/lib/icfs/cache_elastic.rb +1154 -0
- data/lib/icfs/demo/auth.rb +74 -0
- data/lib/icfs/demo/static.rb +59 -0
- data/lib/icfs/demo/timezone.rb +38 -0
- data/lib/icfs/elastic.rb +83 -0
- data/lib/icfs/items.rb +653 -0
- data/lib/icfs/store.rb +278 -0
- data/lib/icfs/store_fs.rb +98 -0
- data/lib/icfs/store_s3.rb +97 -0
- data/lib/icfs/users.rb +80 -0
- data/lib/icfs/users_elastic.rb +166 -0
- data/lib/icfs/users_fs.rb +132 -0
- data/lib/icfs/validate.rb +479 -0
- data/lib/icfs/web/auth_ssl.rb +73 -0
- data/lib/icfs/web/client.rb +4498 -0
- metadata +77 -0
data/lib/icfs/cache.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
#
|
2
|
+
# Investigative Case File System
|
3
|
+
#
|
4
|
+
# Copyright 2019 by Graham A. Field
|
5
|
+
#
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License version 3.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
#
|
13
|
+
module ICFS
|
14
|
+
|
15
|
+
##########################################################################
|
16
|
+
# Stores current items and provides a standard search interface.
|
17
|
+
#
|
18
|
+
# Stores:
|
19
|
+
# * Case - current version
|
20
|
+
# * Log - all
|
21
|
+
# * Entry - current version
|
22
|
+
# * Action - current version
|
23
|
+
# * Current - for each case
|
24
|
+
# * Index - current version
|
25
|
+
#
|
26
|
+
# Provides locking and searching interface.
|
27
|
+
#
|
28
|
+
# @abstract
|
29
|
+
#
|
30
|
+
class Cache
|
31
|
+
|
32
|
+
###############################################
|
33
|
+
# What searching options are supported
|
34
|
+
#
|
35
|
+
# @return [Hash] Supported searching options
|
36
|
+
#
|
37
|
+
def supports; raise NotImplementedError; end
|
38
|
+
|
39
|
+
|
40
|
+
###############################################
|
41
|
+
# Take a case lock
|
42
|
+
#
|
43
|
+
# @param cid [String] caseid
|
44
|
+
#
|
45
|
+
def lock_take(cid); raise NotImplementedError; end
|
46
|
+
|
47
|
+
|
48
|
+
###############################################
|
49
|
+
# Release a case lock
|
50
|
+
#
|
51
|
+
# @param cid [String] caseid
|
52
|
+
#
|
53
|
+
def lock_release(cid); raise NotImplementedError; end
|
54
|
+
|
55
|
+
|
56
|
+
###############################################
|
57
|
+
# Read current
|
58
|
+
#
|
59
|
+
# @param cid [String] caseid
|
60
|
+
# @return [String] JSON encoded item
|
61
|
+
#
|
62
|
+
def current_read(cid); raise NotImplementedError; end
|
63
|
+
|
64
|
+
|
65
|
+
###############################################
|
66
|
+
# Write current
|
67
|
+
#
|
68
|
+
# @param cid [String] caseid
|
69
|
+
# @param item [String] JSON encoded item
|
70
|
+
#
|
71
|
+
def current_write(cid, item); raise NotImplementedError; end
|
72
|
+
|
73
|
+
|
74
|
+
###############################################
|
75
|
+
# Read a case
|
76
|
+
#
|
77
|
+
# @param cid [String] caseid
|
78
|
+
# @return [String] JSON encoded item
|
79
|
+
#
|
80
|
+
def case_read(cid); raise NotImplementedError; end
|
81
|
+
|
82
|
+
|
83
|
+
###############################################
|
84
|
+
# Write a case
|
85
|
+
#
|
86
|
+
# @param cid [String] caseid
|
87
|
+
# @param item [String] JSON encoded item
|
88
|
+
#
|
89
|
+
def case_write(cid, item); raise NotImplementedError; end
|
90
|
+
|
91
|
+
|
92
|
+
###############################################
|
93
|
+
# Search for cases
|
94
|
+
#
|
95
|
+
# @param query [Hash] the query
|
96
|
+
#
|
97
|
+
def case_search(query); raise NotImplementedError; end
|
98
|
+
|
99
|
+
|
100
|
+
###############################################
|
101
|
+
# Get list of tags for cases
|
102
|
+
#
|
103
|
+
# @param query [Hash] the query
|
104
|
+
#
|
105
|
+
def case_tags(query); raise NotImplementedError;end
|
106
|
+
|
107
|
+
|
108
|
+
###############################################
|
109
|
+
# Read an entry
|
110
|
+
#
|
111
|
+
# @param cid [String] caseid
|
112
|
+
# @param enum [Integer] the entry number
|
113
|
+
# @return [String] JSON encoded item
|
114
|
+
#
|
115
|
+
def entry_read(cid, enum); raise NotImplementedError; end
|
116
|
+
|
117
|
+
|
118
|
+
###############################################
|
119
|
+
# Write an entry
|
120
|
+
#
|
121
|
+
# @param cid [String] caseid
|
122
|
+
# @param enum [Integer] the entry number
|
123
|
+
# @param item [String] JSON encoded item
|
124
|
+
#
|
125
|
+
def entry_write(cid, enum, item); raise NotImplementedError; end
|
126
|
+
|
127
|
+
|
128
|
+
###############################################
|
129
|
+
# Search for entries
|
130
|
+
#
|
131
|
+
# @param query [Hash] the query
|
132
|
+
#
|
133
|
+
def entry_search(query); raise NotImplementedError; end
|
134
|
+
|
135
|
+
|
136
|
+
###############################################
|
137
|
+
# List tags used on Entries
|
138
|
+
#
|
139
|
+
# @param query [Hash] the query
|
140
|
+
#
|
141
|
+
def entry_tags(query); raise NotImplementedError; end
|
142
|
+
|
143
|
+
|
144
|
+
###############################################
|
145
|
+
# Read an action
|
146
|
+
#
|
147
|
+
# @param cid [String] caseid
|
148
|
+
# @param anum [Integer] the action number
|
149
|
+
# @return [String] JSON encoded item
|
150
|
+
#
|
151
|
+
def action_read(cid, anum); raise NotImplementedError; end
|
152
|
+
|
153
|
+
|
154
|
+
###############################################
|
155
|
+
# Write an action
|
156
|
+
#
|
157
|
+
# @param cid [String] caseid
|
158
|
+
# @param anum [Integer] the action number
|
159
|
+
# @param item [String] JSON encoded item
|
160
|
+
#
|
161
|
+
def action_write(cid, anum, item); raise NotImplementedError; end
|
162
|
+
|
163
|
+
|
164
|
+
###############################################
|
165
|
+
# Search for actions
|
166
|
+
#
|
167
|
+
# @param query [Hash] the query
|
168
|
+
#
|
169
|
+
def action_search(query); raise NotImplementedError; end
|
170
|
+
|
171
|
+
|
172
|
+
###############################################
|
173
|
+
# List tags used on action tasks
|
174
|
+
#
|
175
|
+
# @param query [Hash] the query
|
176
|
+
#
|
177
|
+
def action_tags(query); raise NotImplementedError; end
|
178
|
+
|
179
|
+
|
180
|
+
###############################################
|
181
|
+
# Read an Index
|
182
|
+
#
|
183
|
+
# @param cid [String] caseid
|
184
|
+
# @param xnum [Integer] the index number
|
185
|
+
# @return [String] JSON encoded item
|
186
|
+
#
|
187
|
+
def index_read(cid, xnum); raise NotImplementedError; end
|
188
|
+
|
189
|
+
|
190
|
+
###############################################
|
191
|
+
# Write an Index
|
192
|
+
#
|
193
|
+
# @param cid [String] caseid
|
194
|
+
# @param xnum [Integer] the index number
|
195
|
+
# @param item [String] JSON encoded item
|
196
|
+
#
|
197
|
+
def index_write(cid, xnum, item); raise NotImplementedError; end
|
198
|
+
|
199
|
+
|
200
|
+
###############################################
|
201
|
+
# Search for Indexes
|
202
|
+
#
|
203
|
+
# @param query [Hash] the query
|
204
|
+
#
|
205
|
+
def index_search(query); raise NotImplementedError; end
|
206
|
+
|
207
|
+
|
208
|
+
###############################################
|
209
|
+
# List tags used in indexes
|
210
|
+
#
|
211
|
+
# @param query [Hash] the query
|
212
|
+
#
|
213
|
+
def index_tags(query); raise NotImplementedError; end
|
214
|
+
|
215
|
+
|
216
|
+
###############################################
|
217
|
+
# Read a log
|
218
|
+
#
|
219
|
+
# @param cid [String] caseid
|
220
|
+
# @param lnum [Integer] the log number
|
221
|
+
# @return [String] JSON encoded item
|
222
|
+
#
|
223
|
+
def log_read(cid, lnum); raise NotImplementedError; end
|
224
|
+
|
225
|
+
|
226
|
+
###############################################
|
227
|
+
# Write a log
|
228
|
+
#
|
229
|
+
# @param cid [String] caseid
|
230
|
+
# @param lnum [Integer] the log number
|
231
|
+
# @param item [String] JSON encoded item
|
232
|
+
#
|
233
|
+
def log_write(cid, lnum, item); raise NotImplementedError; end
|
234
|
+
|
235
|
+
|
236
|
+
###############################################
|
237
|
+
# Search for a log
|
238
|
+
#
|
239
|
+
# @param query [Hash] the query
|
240
|
+
#
|
241
|
+
def log_search(query); raise NotImplementedError; end
|
242
|
+
|
243
|
+
|
244
|
+
###############################################
|
245
|
+
# Analyze stats
|
246
|
+
#
|
247
|
+
# @param query [Hash] the query
|
248
|
+
#
|
249
|
+
def stats(query); raise NotImplementedError; end
|
250
|
+
|
251
|
+
|
252
|
+
end # class ICFS::Cache
|
253
|
+
|
254
|
+
end # module ICFS
|
@@ -0,0 +1,1154 @@
|
|
1
|
+
#
|
2
|
+
# Investigative Case File System
|
3
|
+
#
|
4
|
+
# Copyright 2019 by Graham A. Field
|
5
|
+
#
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License version 3.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
require_relative 'elastic'
|
13
|
+
|
14
|
+
|
15
|
+
module ICFS
|
16
|
+
|
17
|
+
##########################################################################
|
18
|
+
# Implements {ICFS::Cache Cache} using Elasticsearch
|
19
|
+
#
|
20
|
+
class CacheElastic < Cache
|
21
|
+
|
22
|
+
include Elastic
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
###############################################
|
27
|
+
# The ES mappings for all of the indexes
|
28
|
+
Maps = {
|
29
|
+
:case => '{
|
30
|
+
"mappings": { "_doc": { "properties": {
|
31
|
+
"icfs": { "enabled": false },
|
32
|
+
"caseid": { "type": "keyword" },
|
33
|
+
"log": { "enabled": false },
|
34
|
+
"template": { "type": "boolean" },
|
35
|
+
"status": { "type": "boolean" },
|
36
|
+
"title": { "type": "text" },
|
37
|
+
"tags": { "type": "keyword" },
|
38
|
+
"access": { "type": "nested", "properties": {
|
39
|
+
"perm": { "type": "keyword" },
|
40
|
+
"grant": { "type": "keyword" }
|
41
|
+
}}}
|
42
|
+
}}}
|
43
|
+
}'.freeze,
|
44
|
+
|
45
|
+
:log => '{
|
46
|
+
"mappings": { "_doc": { "properties": {
|
47
|
+
"icfs": { "enabled": false },
|
48
|
+
"caseid": {"type": "keyword" },
|
49
|
+
"log": { "type": "integer" },
|
50
|
+
"prev": { "enabled": false },
|
51
|
+
"time": { "type": "date", "format": "epoch_second" },
|
52
|
+
"user": { "type": "keyword" },
|
53
|
+
"entry": { "properties": {
|
54
|
+
"num": { "type": "integer" },
|
55
|
+
"hash": { "enabled": false }
|
56
|
+
}},
|
57
|
+
"index": { "properties": {
|
58
|
+
"num": { "type": "integer" },
|
59
|
+
"hash": { "enabled": false }
|
60
|
+
}},
|
61
|
+
"action": { "properties": {
|
62
|
+
"num": { "type": "integer" },
|
63
|
+
"hash": { "enabled": false }
|
64
|
+
}},
|
65
|
+
"case_hash": { "enabled": false },
|
66
|
+
"files_hash": { "enabled": false }
|
67
|
+
}}}
|
68
|
+
}'.freeze,
|
69
|
+
|
70
|
+
:entry => '{
|
71
|
+
"mappings": { "_doc": { "properties": {
|
72
|
+
"icfs": { "enabled": false },
|
73
|
+
"caseid": { "type": "keyword" },
|
74
|
+
"entry": { "type": "integer" },
|
75
|
+
"log": { "enabled": false },
|
76
|
+
"user": { "type": "keyword" },
|
77
|
+
"time": { "type": "date", "format": "epoch_second" },
|
78
|
+
"title": { "type": "text" },
|
79
|
+
"content": { "type": "text" },
|
80
|
+
"tags": { "type": "keyword" },
|
81
|
+
"index": { "type": "integer" },
|
82
|
+
"action": { "type": "integer" },
|
83
|
+
"perms": { "type": "keyword" },
|
84
|
+
"stats": { "type": "nested", "properties": {
|
85
|
+
"name": { "type": "keyword" },
|
86
|
+
"value": { "type": "double" },
|
87
|
+
"credit": { "type": "keyword" }
|
88
|
+
}},
|
89
|
+
"files": { "properties": {
|
90
|
+
"log": { "enabled": false },
|
91
|
+
"num": { "enabled": false },
|
92
|
+
"name": { "type": "text" }
|
93
|
+
}}
|
94
|
+
}}}
|
95
|
+
}'.freeze,
|
96
|
+
|
97
|
+
:action => '{
|
98
|
+
"mappings": { "_doc": { "properties": {
|
99
|
+
"icfs": { "enabled": false },
|
100
|
+
"caseid": { "type": "keyword" },
|
101
|
+
"action": { "type": "integer" },
|
102
|
+
"log": { "enabled": false },
|
103
|
+
"tasks": { "type": "nested","properties": {
|
104
|
+
"assigned": { "type": "keyword" },
|
105
|
+
"title": { "type": "text" },
|
106
|
+
"status": { "type": "boolean" },
|
107
|
+
"flag": { "type": "boolean" },
|
108
|
+
"time": { "type": "date", "format": "epoch_second" },
|
109
|
+
"tags": { "type": "keyword" }
|
110
|
+
}}
|
111
|
+
}}}
|
112
|
+
}'.freeze,
|
113
|
+
|
114
|
+
:index => '{
|
115
|
+
"mappings": { "_doc": { "properties": {
|
116
|
+
"icfs": { "enabled": false },
|
117
|
+
"caseid": { "type": "keyword" },
|
118
|
+
"index": { "type": "integer" },
|
119
|
+
"log": { "enabled": false },
|
120
|
+
"title": {
|
121
|
+
"type": "text",
|
122
|
+
"fields": { "raw": { "type": "keyword" }}
|
123
|
+
},
|
124
|
+
"content": { "type": "text" },
|
125
|
+
"tags": { "type": "keyword" }
|
126
|
+
}}}
|
127
|
+
}'.freeze,
|
128
|
+
|
129
|
+
:current => '{
|
130
|
+
"mappings": {"_doc": {
|
131
|
+
"enabled": false
|
132
|
+
}}
|
133
|
+
}'.freeze,
|
134
|
+
|
135
|
+
:lock => '{
|
136
|
+
"mappings": { "_doc": {
|
137
|
+
"enabled": false
|
138
|
+
}}
|
139
|
+
}'.freeze,
|
140
|
+
}.freeze
|
141
|
+
|
142
|
+
|
143
|
+
public
|
144
|
+
|
145
|
+
|
146
|
+
###############################################
|
147
|
+
# New instance
|
148
|
+
#
|
149
|
+
# @param map [Hash] Symbol to String of the indexes. Must provide
|
150
|
+
# :case, :log, :entry, :action, :current, and :lock
|
151
|
+
# @param es [Faraday] Faraday instance to the Elasticsearch cluster
|
152
|
+
#
|
153
|
+
def initialize(map, es)
|
154
|
+
@map = map
|
155
|
+
@es = es
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
###############################################
|
160
|
+
# (see Cache#supports)
|
161
|
+
#
|
162
|
+
#def supports; raise NotImplementedError; end
|
163
|
+
|
164
|
+
|
165
|
+
###############################################
|
166
|
+
# (see Cache#lock_take)
|
167
|
+
#
|
168
|
+
# @todo Include client info to help with debugging
|
169
|
+
#
|
170
|
+
def lock_take(cid)
|
171
|
+
|
172
|
+
json = '{"client":"TODO"}'.freeze
|
173
|
+
url = '%s/_doc/%s/_create'.freeze % [@map[:lock], CGI.escape(cid)]
|
174
|
+
head = {'Content-Type'.freeze => 'application/json'.freeze}.freeze
|
175
|
+
|
176
|
+
# try to take
|
177
|
+
tries = 5
|
178
|
+
while tries > 0
|
179
|
+
resp = @es.run_request(:put, url, json, head)
|
180
|
+
return true if resp.success?
|
181
|
+
tries = tries - 1
|
182
|
+
sleep(0.1)
|
183
|
+
end
|
184
|
+
|
185
|
+
# failed to take lock
|
186
|
+
raise('Elasticsearch lock take failed: %s'.freeze % cid)
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
###############################################
|
191
|
+
# (see Cache#lock_release)
|
192
|
+
#
|
193
|
+
def lock_release(cid)
|
194
|
+
url = '%s/_doc/%s'.freeze % [@map[:lock], CGI.escape(cid)]
|
195
|
+
resp = @es.run_request(:delete, url, '', {})
|
196
|
+
if !resp.success?
|
197
|
+
raise('Elasticsearch lock release failed: %s'.freeze % cid)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
###############################################
|
203
|
+
# (see Cache#current_read)
|
204
|
+
#
|
205
|
+
def current_read(cid)
|
206
|
+
_read(:current, cid)
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
###############################################
|
211
|
+
# (see Cache#current_write)
|
212
|
+
#
|
213
|
+
def current_write(cid, item)
|
214
|
+
_write(:current, cid, item)
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
###############################################
|
219
|
+
# (see Cache#case_read)
|
220
|
+
#
|
221
|
+
def case_read(cid)
|
222
|
+
_read(:case, cid)
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
###############################################
|
227
|
+
# (see Cache#case_write)
|
228
|
+
#
|
229
|
+
def case_write(cid, item)
|
230
|
+
_write(:case, cid, item)
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
###############################################
|
235
|
+
# match query
|
236
|
+
#
|
237
|
+
def _query_match(field, val)
|
238
|
+
return nil if !val
|
239
|
+
{ 'match' => { field => { 'query' => val } } }
|
240
|
+
end # def _query_match()
|
241
|
+
|
242
|
+
|
243
|
+
###############################################
|
244
|
+
# match all query
|
245
|
+
#
|
246
|
+
def _query_all()
|
247
|
+
{ 'match_all' => {} }
|
248
|
+
end # def _query_all()
|
249
|
+
|
250
|
+
|
251
|
+
###############################################
|
252
|
+
# (see Cache#case_search)
|
253
|
+
#
|
254
|
+
def case_search(query)
|
255
|
+
|
256
|
+
# build the query
|
257
|
+
must = [
|
258
|
+
_query_match('title'.freeze, query[:title]),
|
259
|
+
].compact
|
260
|
+
filter = [
|
261
|
+
_query_term('tags'.freeze, query[:tags]),
|
262
|
+
_query_term('status'.freeze, query[:status]),
|
263
|
+
_query_term('template'.freeze, query[:template]),
|
264
|
+
].compact
|
265
|
+
access = [
|
266
|
+
_query_term('access.grant'.freeze, query[:grantee]),
|
267
|
+
_query_term('access.perm'.freeze, query[:perm]),
|
268
|
+
].compact
|
269
|
+
unless access.empty?
|
270
|
+
qu = (access.size == 1) ? access[0] : _query_bool(nil, access, nil, nil)
|
271
|
+
filter << _query_nested('access'.freeze, qu)
|
272
|
+
end
|
273
|
+
req = { 'query' => _query_bool(must, filter, nil, nil) }
|
274
|
+
|
275
|
+
# highlight
|
276
|
+
hl = {}
|
277
|
+
hl['title'] = {} if query[:title]
|
278
|
+
req['highlight'] = { 'fields' => hl } unless hl.empty?
|
279
|
+
|
280
|
+
# sort
|
281
|
+
unless query[:title]
|
282
|
+
req['sort'] = { 'caseid' => 'asc' }
|
283
|
+
end
|
284
|
+
|
285
|
+
# paging
|
286
|
+
_page(query, req)
|
287
|
+
|
288
|
+
# run the search
|
289
|
+
url = @map[:case] + '/_search'.freeze
|
290
|
+
body = JSON.generate(req)
|
291
|
+
head = { 'Content-Type' => 'application/json' }
|
292
|
+
resp = @es.run_request(:get, url, body, head)
|
293
|
+
raise 'search failed' if !resp.success?
|
294
|
+
|
295
|
+
return _results(resp, query, ResultsCase)
|
296
|
+
end # def case_search
|
297
|
+
|
298
|
+
|
299
|
+
# the Case results fields
|
300
|
+
ResultsCase = {
|
301
|
+
caseid: 'caseid'.freeze,
|
302
|
+
template: 'template'.freeze,
|
303
|
+
status: 'status'.freeze,
|
304
|
+
title: 'title'.freeze,
|
305
|
+
tags: 'tags'.freeze,
|
306
|
+
}.freeze
|
307
|
+
|
308
|
+
|
309
|
+
###############################################
|
310
|
+
# Process search results
|
311
|
+
#
|
312
|
+
# @param resp [Hash] the response from Elasticsearch
|
313
|
+
# @param query [Hash] the original request
|
314
|
+
# @param fields [Hash] Fields to return
|
315
|
+
# @yield [src] The source object
|
316
|
+
# @yieldreturn [Hash] the search result object
|
317
|
+
#
|
318
|
+
def _results(resp, query, fields=nil)
|
319
|
+
|
320
|
+
# size defaults to 25
|
321
|
+
size = query[:size] ? query[:size].to_i : 0
|
322
|
+
size = DefaultSize if size == 0
|
323
|
+
|
324
|
+
rh = JSON.parse(resp.body)
|
325
|
+
results = {
|
326
|
+
query: query,
|
327
|
+
hits: rh['hits']['total'],
|
328
|
+
size: size,
|
329
|
+
}
|
330
|
+
|
331
|
+
# process each result
|
332
|
+
results[:list] = rh['hits']['hits'].map do |hh|
|
333
|
+
|
334
|
+
src = hh['_source']
|
335
|
+
hl = hh['highlight']
|
336
|
+
|
337
|
+
if hl
|
338
|
+
snip = ''
|
339
|
+
hl.each{|fn, ary| ary.each{|ht| snip << ht}}
|
340
|
+
else
|
341
|
+
snip = nil
|
342
|
+
end
|
343
|
+
|
344
|
+
# fields provided
|
345
|
+
if fields
|
346
|
+
obj = {}
|
347
|
+
fields.each do |aa, bb|
|
348
|
+
if bb.is_a?(Array)
|
349
|
+
case bb[1]
|
350
|
+
|
351
|
+
# a sub value
|
352
|
+
when :sub
|
353
|
+
val = src[bb[0]]
|
354
|
+
obj[aa] = val.nil? ? 0 : val[bb[2]]
|
355
|
+
|
356
|
+
# size of a value
|
357
|
+
when :size
|
358
|
+
val = src[bb[0]]
|
359
|
+
obj[aa] = val.nil? ? 0 : val.size
|
360
|
+
|
361
|
+
# zero for nil
|
362
|
+
when :zero
|
363
|
+
val = src[bb[0]]
|
364
|
+
obj[aa] = val.nil? ? 0 : val
|
365
|
+
|
366
|
+
# empty array for nil
|
367
|
+
when :empty
|
368
|
+
val = src[bb[0]]
|
369
|
+
obj[aa] = val.nil? ? [] : val
|
370
|
+
|
371
|
+
else
|
372
|
+
raise(ArgumentError, 'Not a valid field option'.freeze)
|
373
|
+
end
|
374
|
+
else
|
375
|
+
obj[aa] = src[bb]
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# pass the source to the block to generate the search object
|
380
|
+
else
|
381
|
+
obj = yield src
|
382
|
+
end
|
383
|
+
|
384
|
+
# and provide each result
|
385
|
+
{
|
386
|
+
score: hh['_score'],
|
387
|
+
snippet: snip,
|
388
|
+
object: obj,
|
389
|
+
}
|
390
|
+
end
|
391
|
+
|
392
|
+
return results
|
393
|
+
end # def _results()
|
394
|
+
private :_results
|
395
|
+
|
396
|
+
|
397
|
+
# default page size
|
398
|
+
DefaultSize = 25
|
399
|
+
|
400
|
+
|
401
|
+
###############################################
|
402
|
+
# Do paging
|
403
|
+
#
|
404
|
+
# @param query [Hash] the query
|
405
|
+
# @param req [Hash] the constructed ES request
|
406
|
+
#
|
407
|
+
def _page(query, req)
|
408
|
+
|
409
|
+
# size defaults
|
410
|
+
size = query[:size] ? query[:size].to_i : 0
|
411
|
+
size = DefaultSize if size == 0
|
412
|
+
|
413
|
+
# page defaults to 1
|
414
|
+
page = query[:page] ? query[:page].to_i : 0
|
415
|
+
page = 1 if page == 0
|
416
|
+
|
417
|
+
req['size'] = size
|
418
|
+
req['from'] = (page - 1) * size
|
419
|
+
|
420
|
+
end # def _page()
|
421
|
+
private :_page
|
422
|
+
|
423
|
+
|
424
|
+
|
425
|
+
###############################################
|
426
|
+
# (see Cache#entry_read)
|
427
|
+
#
|
428
|
+
def entry_read(cid, enum)
|
429
|
+
_read(:entry, '%s.%d'.freeze % [cid, enum])
|
430
|
+
end
|
431
|
+
|
432
|
+
|
433
|
+
###############################################
|
434
|
+
# (see Cache#entry_write)
|
435
|
+
#
|
436
|
+
def entry_write(cid, enum, item)
|
437
|
+
_write(:entry, '%s.%d'.freeze % [cid, enum], item)
|
438
|
+
end
|
439
|
+
|
440
|
+
|
441
|
+
###############################################
|
442
|
+
# Nested query
|
443
|
+
#
|
444
|
+
def _query_nested(field, query)
|
445
|
+
{
|
446
|
+
'nested' => {
|
447
|
+
'path' => field,
|
448
|
+
'query' => query
|
449
|
+
}
|
450
|
+
}
|
451
|
+
end # def _query_nested()
|
452
|
+
|
453
|
+
|
454
|
+
###############################################
|
455
|
+
# (see Cache#entry_search)
|
456
|
+
#
|
457
|
+
def entry_search(query)
|
458
|
+
|
459
|
+
# build the query
|
460
|
+
must = [
|
461
|
+
_query_match('title'.freeze, query[:title]),
|
462
|
+
_query_match('content'.freeze, query[:content]),
|
463
|
+
].compact
|
464
|
+
filter = [
|
465
|
+
_query_term('tags'.freeze, query[:tags]),
|
466
|
+
_query_term('caseid'.freeze, query[:caseid]),
|
467
|
+
_query_times('time'.freeze, query[:after], query[:before]),
|
468
|
+
_query_term('action'.freeze, query[:action]),
|
469
|
+
_query_term('index'.freeze, query[:index]),
|
470
|
+
].compact
|
471
|
+
stats = [
|
472
|
+
_query_term('stats.name'.freeze, query[:stat]),
|
473
|
+
_query_term('stats.credit'.freeze, query[:credit]),
|
474
|
+
].compact
|
475
|
+
unless stats.empty?
|
476
|
+
qu = (stats.size == 1) ? stats[0] : _query_bool(nil, stats, nil, nil)
|
477
|
+
filter << _query_nested('stats'.freeze, qu)
|
478
|
+
end
|
479
|
+
req = { 'query' => _query_bool(must, filter, nil, nil) }
|
480
|
+
|
481
|
+
# highlight
|
482
|
+
hl = {}
|
483
|
+
hl['title'] = {} if query[:title]
|
484
|
+
hl['content'] = {} if query[:content]
|
485
|
+
req['highlight'] = { 'fields' => hl } unless hl.empty?
|
486
|
+
|
487
|
+
# sort
|
488
|
+
case query[:sort]
|
489
|
+
when 'time_desc'
|
490
|
+
req['sort'] = [
|
491
|
+
{ 'time' => 'desc' },
|
492
|
+
{ '_id' => 'desc' },
|
493
|
+
]
|
494
|
+
when 'time_asc'
|
495
|
+
req['sort'] = [
|
496
|
+
{ 'time' => 'asc' },
|
497
|
+
{ '_id' => 'desc' },
|
498
|
+
]
|
499
|
+
when nil
|
500
|
+
if !query[:title] && !query[:content]
|
501
|
+
req['sort'] = [
|
502
|
+
{ 'time' => 'desc' },
|
503
|
+
{ '_id' => 'desc' },
|
504
|
+
]
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
# paging
|
509
|
+
_page(query, req)
|
510
|
+
|
511
|
+
# run the search
|
512
|
+
url = @map[:entry] + '/_search'.freeze
|
513
|
+
body = JSON.generate(req)
|
514
|
+
head = { 'Content-Type' => 'application/json' }
|
515
|
+
resp = @es.run_request(:get, url, body, head)
|
516
|
+
raise 'search failed' if !resp.success?
|
517
|
+
|
518
|
+
return _results(resp, query, ResultsEntry)
|
519
|
+
end # def entry_search()
|
520
|
+
|
521
|
+
|
522
|
+
# Entry search results fields
|
523
|
+
ResultsEntry = {
|
524
|
+
caseid: 'caseid'.freeze,
|
525
|
+
entry: 'entry'.freeze,
|
526
|
+
time: 'time'.freeze,
|
527
|
+
title: 'title'.freeze,
|
528
|
+
tags: 'tags'.freeze,
|
529
|
+
perms: ['perms'.freeze, :empty],
|
530
|
+
action: ['action'.freeze, :zero],
|
531
|
+
index: ['index'.freeze, :size],
|
532
|
+
files: ['files'.freeze, :size],
|
533
|
+
stats: ['stats'.freeze, :size],
|
534
|
+
}.freeze
|
535
|
+
|
536
|
+
|
537
|
+
|
538
|
+
###############################################
|
539
|
+
# (see Cache#action_read)
|
540
|
+
#
|
541
|
+
def action_read(cid, anum)
|
542
|
+
_read(:action, '%s.%d'.freeze % [cid, anum])
|
543
|
+
end
|
544
|
+
|
545
|
+
|
546
|
+
###############################################
|
547
|
+
# (see Cache#action_write)
|
548
|
+
#
|
549
|
+
def action_write(cid, anum, item)
|
550
|
+
_write(:action, '%s.%d'.freeze % [cid, anum], item)
|
551
|
+
end
|
552
|
+
|
553
|
+
|
554
|
+
###############################################
|
555
|
+
# (see Cache#action_search)
|
556
|
+
#
|
557
|
+
def action_search(query)
|
558
|
+
|
559
|
+
# build the query
|
560
|
+
task_must = [
|
561
|
+
_query_match('tasks.title'.freeze, query[:title])
|
562
|
+
].compact
|
563
|
+
task_filter = [
|
564
|
+
_query_term('tasks.assigned'.freeze, query[:assigned]),
|
565
|
+
_query_term('tasks.status'.freeze, query[:status]),
|
566
|
+
_query_term('tasks.flag'.freeze, query[:flag]),
|
567
|
+
_query_times('tasks.time'.freeze, query[:after], query[:before]),
|
568
|
+
_query_term('tasks.tags'.freeze, query[:tags]),
|
569
|
+
].compact
|
570
|
+
must = [
|
571
|
+
_query_nested(
|
572
|
+
'tasks'.freeze,
|
573
|
+
_query_bool(task_must, task_filter, nil, nil)
|
574
|
+
)
|
575
|
+
]
|
576
|
+
filter = [
|
577
|
+
_query_term('caseid'.freeze, query[:caseid])
|
578
|
+
].compact
|
579
|
+
req = { 'query' => _query_bool(must, filter, nil, nil) }
|
580
|
+
|
581
|
+
# sort
|
582
|
+
case query[:sort]
|
583
|
+
when 'time_desc'
|
584
|
+
srt = 'desc'
|
585
|
+
when 'time_asc'
|
586
|
+
srt = 'asc'
|
587
|
+
else
|
588
|
+
srt = query[:title] ? nil : 'desc'
|
589
|
+
end
|
590
|
+
if srt
|
591
|
+
req['sort'] = [
|
592
|
+
{
|
593
|
+
'tasks.time' => {
|
594
|
+
'order' => srt,
|
595
|
+
'nested' => {
|
596
|
+
'path' => 'tasks'.freeze,
|
597
|
+
'filter' => _query_term(
|
598
|
+
'tasks.assigned'.freeze, query[:assigned])
|
599
|
+
}
|
600
|
+
}
|
601
|
+
},
|
602
|
+
{ '_id' => { 'order' => 'desc' } }
|
603
|
+
]
|
604
|
+
end
|
605
|
+
|
606
|
+
# paging
|
607
|
+
_page(query, req)
|
608
|
+
|
609
|
+
# run the search
|
610
|
+
url = @map[:action] + '/_search'.freeze
|
611
|
+
body = JSON.generate(req)
|
612
|
+
head = { 'Content-Type' => 'application/json' }
|
613
|
+
resp = @es.run_request(:get, url, body, head)
|
614
|
+
raise 'search failed' if !resp.success?
|
615
|
+
|
616
|
+
return _results(resp, query) do |src|
|
617
|
+
tsk = src['tasks'].select{|tk| tk['assigned'] == query[:assigned]}.first
|
618
|
+
{
|
619
|
+
caseid: src['caseid'],
|
620
|
+
action: src['action'],
|
621
|
+
status: tsk['status'],
|
622
|
+
flag: tsk['flag'],
|
623
|
+
title: tsk['title'],
|
624
|
+
time: tsk['time'],
|
625
|
+
tags: tsk['tags'],
|
626
|
+
}
|
627
|
+
end
|
628
|
+
end # def action_search()
|
629
|
+
|
630
|
+
|
631
|
+
###############################################
|
632
|
+
# (see Cache#index_write)
|
633
|
+
#
|
634
|
+
def index_write(cid, xnum, item)
|
635
|
+
_write(:index, '%s.%d'.freeze % [cid, xnum], item)
|
636
|
+
end
|
637
|
+
|
638
|
+
|
639
|
+
###############################################
|
640
|
+
# (see Cache#index_read)
|
641
|
+
#
|
642
|
+
def index_read(cid, xnum)
|
643
|
+
_read(:index, '%s.%d'.freeze % [cid, xnum])
|
644
|
+
end
|
645
|
+
|
646
|
+
|
647
|
+
# (see Cache#index_search)
|
648
|
+
#
|
649
|
+
def index_search(query)
|
650
|
+
|
651
|
+
# build the query
|
652
|
+
must = [
|
653
|
+
_query_match('title'.freeze, query[:title]),
|
654
|
+
_query_match('content'.freeze, query[:content]),
|
655
|
+
].compact
|
656
|
+
filter = [
|
657
|
+
_query_term('caseid'.freeze, query[:caseid]),
|
658
|
+
_query_term('tags'.freeze, query[:tags]),
|
659
|
+
_query_prefix('title'.freeze, query[:prefix]),
|
660
|
+
].compact
|
661
|
+
req = { 'query' => _query_bool(must, filter, nil, nil) }
|
662
|
+
|
663
|
+
# highlight
|
664
|
+
hl = {}
|
665
|
+
hl['title'] = {} if query[:title]
|
666
|
+
hl['content'] = {} if query[:content]
|
667
|
+
req['highlight'] = { 'fields' => hl } unless hl.empty?
|
668
|
+
|
669
|
+
# sort
|
670
|
+
case query[:sort]
|
671
|
+
when 'index_asc'
|
672
|
+
req['sort'] = [
|
673
|
+
{ 'index' => 'asc' },
|
674
|
+
{ '_id' => 'desc' },
|
675
|
+
]
|
676
|
+
when 'index_desc'
|
677
|
+
req['sort'] = [
|
678
|
+
{ 'index' => 'desc' },
|
679
|
+
{ '_id' => 'desc' },
|
680
|
+
]
|
681
|
+
when 'title_desc'
|
682
|
+
req['sort'] = [
|
683
|
+
{ 'title.raw' => 'desc' },
|
684
|
+
{ '_id' => 'desc' },
|
685
|
+
]
|
686
|
+
when 'title_asc', nil
|
687
|
+
req['sort'] = [
|
688
|
+
{ 'title.raw' => 'asc' },
|
689
|
+
{ '_id' => 'desc' },
|
690
|
+
]
|
691
|
+
end
|
692
|
+
|
693
|
+
# paging
|
694
|
+
_page(query, req)
|
695
|
+
|
696
|
+
# run the search
|
697
|
+
url = @map[:index] + '/_search'.freeze
|
698
|
+
body = JSON.generate(req)
|
699
|
+
head = { 'Content-Type' => 'application/json' }
|
700
|
+
resp = @es.run_request(:get, url, body, head)
|
701
|
+
raise 'search failed' if !resp.success?
|
702
|
+
|
703
|
+
return _results(resp, query, ResultsIndex)
|
704
|
+
end # end index_search()
|
705
|
+
|
706
|
+
|
707
|
+
# Index search results fields
|
708
|
+
ResultsIndex = {
|
709
|
+
caseid: 'caseid'.freeze,
|
710
|
+
index: 'index'.freeze,
|
711
|
+
title: 'title'.freeze,
|
712
|
+
tags: 'tags'.freeze,
|
713
|
+
}.freeze
|
714
|
+
|
715
|
+
|
716
|
+
###############################################
|
717
|
+
# (see Cache#index_tags)
|
718
|
+
#
|
719
|
+
def index_tags(query)
|
720
|
+
|
721
|
+
# build the query
|
722
|
+
ag = _agg_terms('tags'.freeze, 'tags'.freeze, nil)
|
723
|
+
qu = _query_term('caseid'.freeze, query[:caseid])
|
724
|
+
qu = _query_constant(qu)
|
725
|
+
req = {
|
726
|
+
'query' => qu,
|
727
|
+
'aggs' => ag,
|
728
|
+
'size' => 0
|
729
|
+
}
|
730
|
+
|
731
|
+
# run the search
|
732
|
+
url = @map[:index] + '/_search'.freeze
|
733
|
+
body = JSON.generate(req)
|
734
|
+
head = { 'Content-Type' => 'application/json'.freeze }
|
735
|
+
resp = @es.run_request(:get, url, body, head)
|
736
|
+
raise 'search failed'.freeze if !resp.success?
|
737
|
+
|
738
|
+
# extract tags
|
739
|
+
rh = JSON.parse(resp.body)
|
740
|
+
rh = rh['aggregations']['tags']['buckets']
|
741
|
+
list = rh.map do |hh|
|
742
|
+
{
|
743
|
+
object: {
|
744
|
+
caseid: query[:caseid],
|
745
|
+
tag: hh['key'],
|
746
|
+
count: hh['doc_count'],
|
747
|
+
}
|
748
|
+
}
|
749
|
+
end
|
750
|
+
|
751
|
+
return {
|
752
|
+
query: query,
|
753
|
+
list: list.sort{|aa, bb| aa[:object][:tag] <=> bb[:object][:tag]}
|
754
|
+
}
|
755
|
+
end # def index_tags()
|
756
|
+
|
757
|
+
|
758
|
+
###############################################
|
759
|
+
# (see Cache#log_read)
|
760
|
+
#
|
761
|
+
def log_read(cid, lnum)
|
762
|
+
_read(:log, '%s.%d'.freeze % [cid, lnum])
|
763
|
+
end
|
764
|
+
|
765
|
+
|
766
|
+
###############################################
|
767
|
+
# (see Cache#log_write)
|
768
|
+
#
|
769
|
+
def log_write(cid, lnum, item)
|
770
|
+
_write(:log, '%s.%d'.freeze % [cid, lnum], item)
|
771
|
+
end
|
772
|
+
|
773
|
+
|
774
|
+
# Log search results fields
|
775
|
+
ResultsLog = {
|
776
|
+
caseid: 'caseid'.freeze,
|
777
|
+
log: 'log'.freeze,
|
778
|
+
time: 'time'.freeze,
|
779
|
+
user: 'user'.freeze,
|
780
|
+
entry: ['entry'.freeze, :sub, 'num'.freeze].freeze,
|
781
|
+
index: ['index'.freeze, :sub, 'num'.freeze].freeze,
|
782
|
+
action: ['action'.freeze, :sub, 'num'.freeze].freeze,
|
783
|
+
files: ['files_hash'.freeze, :size].freeze,
|
784
|
+
}.freeze
|
785
|
+
|
786
|
+
|
787
|
+
###############################################
|
788
|
+
# (see Cache#log_search)
|
789
|
+
#
|
790
|
+
def log_search(query)
|
791
|
+
|
792
|
+
# build the query
|
793
|
+
filter = [
|
794
|
+
_query_term('caseid'.freeze, query[:caseid]),
|
795
|
+
_query_times('times'.freeze, query[:after], query[:before]),
|
796
|
+
_query_term('user'.freeze, query[:user]),
|
797
|
+
_query_term('entry.num'.freeze, query[:entry]),
|
798
|
+
_query_term('index.num'.freeze, query[:index]),
|
799
|
+
_query_term('action.num'.freeze, query[:action]),
|
800
|
+
].compact
|
801
|
+
req = { 'query' => _query_bool(nil, filter, nil, nil) }
|
802
|
+
|
803
|
+
# sort
|
804
|
+
case query[:sort]
|
805
|
+
when 'time_desc', nil
|
806
|
+
req['sort'] = [
|
807
|
+
{ 'time' => 'desc' },
|
808
|
+
{ '_id' => 'desc' },
|
809
|
+
]
|
810
|
+
when 'time_asc'
|
811
|
+
req['sort'] = [
|
812
|
+
{ 'time' => 'asc' },
|
813
|
+
{ '_id' => 'desc' },
|
814
|
+
]
|
815
|
+
end
|
816
|
+
|
817
|
+
# paging
|
818
|
+
_page(query, req)
|
819
|
+
|
820
|
+
# run the search
|
821
|
+
url = @map[:log] + '/_search'.freeze
|
822
|
+
body = JSON.generate(req)
|
823
|
+
head = { 'Content-Type' => 'application/json' }
|
824
|
+
resp = @es.run_request(:get, url, body, head)
|
825
|
+
raise 'search failed' if !resp.success?
|
826
|
+
|
827
|
+
return _results(resp, query, ResultsLog)
|
828
|
+
end # def log_search()
|
829
|
+
|
830
|
+
|
831
|
+
###############################################
|
832
|
+
# stats metric aggregation
|
833
|
+
#
|
834
|
+
def _agg_stats(name, field)
|
835
|
+
{ name => { 'stats' => { 'field' => field } } }
|
836
|
+
end # def _agg_stats()
|
837
|
+
|
838
|
+
|
839
|
+
###############################################
|
840
|
+
# terms bucket aggregation
|
841
|
+
#
|
842
|
+
def _agg_terms(name, field, sub)
|
843
|
+
ag = { name => { 'terms' => { 'field' => field } } }
|
844
|
+
ag[name]['aggs'] = sub if sub
|
845
|
+
return ag
|
846
|
+
end # def _agg_terms()
|
847
|
+
|
848
|
+
|
849
|
+
###############################################
|
850
|
+
# filter bucket aggregation
|
851
|
+
#
|
852
|
+
def _agg_filter(name, qu, sub)
|
853
|
+
ag = { name => { 'filter' => qu } }
|
854
|
+
ag[name]['aggs'] = sub if sub
|
855
|
+
return ag
|
856
|
+
end # def _agg_filter()
|
857
|
+
|
858
|
+
|
859
|
+
###############################################
|
860
|
+
# nested bucket aggregation
|
861
|
+
#
|
862
|
+
def _agg_nested(name, field, sub)
|
863
|
+
ag = { name => { 'nested' => { 'path' => field } } }
|
864
|
+
ag[name]['aggs'] = sub if sub
|
865
|
+
return ag
|
866
|
+
end # def _agg_nested()
|
867
|
+
|
868
|
+
|
869
|
+
###############################################
|
870
|
+
# Term query
|
871
|
+
#
|
872
|
+
def _query_term(field, val)
|
873
|
+
return nil if val.nil?
|
874
|
+
{ 'term' => { field => val } }
|
875
|
+
end # def _query_term()
|
876
|
+
|
877
|
+
|
878
|
+
###############################################
|
879
|
+
# Exists query
|
880
|
+
#
|
881
|
+
def _query_exists(field, val)
|
882
|
+
return nil if val.nil?
|
883
|
+
{ 'exists' => { 'field' => field } }
|
884
|
+
end # def _query_exists()
|
885
|
+
|
886
|
+
|
887
|
+
###############################################
|
888
|
+
# keyword query
|
889
|
+
def _query_keyw(field, val)
|
890
|
+
return nil if val.nil?
|
891
|
+
if val.is_a?(Array)
|
892
|
+
qu = { 'terms' => { field => val } }
|
893
|
+
else
|
894
|
+
qu = {'term' => { field => val } }
|
895
|
+
end
|
896
|
+
return qu
|
897
|
+
end # def _query_keyw()
|
898
|
+
|
899
|
+
|
900
|
+
###############################################
|
901
|
+
# times query
|
902
|
+
def _query_times(field, val_gt, val_lt)
|
903
|
+
return nil if( val_gt.nil? && val_lt.nil? )
|
904
|
+
tq = {}
|
905
|
+
tq['gt'] = val_gt if val_gt
|
906
|
+
tq['lt'] = val_lt if val_lt
|
907
|
+
return {'range' => { field => tq } }
|
908
|
+
end # def _query_times()
|
909
|
+
|
910
|
+
|
911
|
+
###############################################
|
912
|
+
# prefix string query
|
913
|
+
def _query_prefix(field, val)
|
914
|
+
return nil if val.nil?
|
915
|
+
return { 'prefix' => { field => val } }
|
916
|
+
end # def _query_prefix()
|
917
|
+
|
918
|
+
|
919
|
+
###############################################
|
920
|
+
# bool query
|
921
|
+
def _query_bool(must, filter, should, must_not)
|
922
|
+
qu = {}
|
923
|
+
qu['must'] = must if(must && !must.empty?)
|
924
|
+
qu['filter'] = filter if(filter && !filter.empty?)
|
925
|
+
qu['should'] = should if(should && !should.empty?)
|
926
|
+
qu['must_not'] = must_not if(must_not && !must_not.empty?)
|
927
|
+
if qu.empty?
|
928
|
+
return { 'match_all' => {} }
|
929
|
+
else
|
930
|
+
return { 'bool' => qu }
|
931
|
+
end
|
932
|
+
end # def _query_bool()
|
933
|
+
|
934
|
+
|
935
|
+
###############################################
|
936
|
+
# (see Cache#stats)
|
937
|
+
#
|
938
|
+
def stats(query)
|
939
|
+
|
940
|
+
# aggs
|
941
|
+
ag = _agg_stats('vals'.freeze, 'stats.value'.freeze)
|
942
|
+
ag = _agg_terms('stats'.freeze, 'stats.name'.freeze, ag)
|
943
|
+
if query[:credit]
|
944
|
+
cd = _query_term('stats.credit'.freeze, query[:credit])
|
945
|
+
ag = _agg_filter('credit'.freeze, cd, ag)
|
946
|
+
end
|
947
|
+
ag = _agg_nested('nested'.freeze, 'stats'.freeze, ag)
|
948
|
+
|
949
|
+
# build the query
|
950
|
+
filt = [
|
951
|
+
_query_term('caseid'.freeze, query[:caseid]),
|
952
|
+
_query_times('time'.freeze, query[:after], query[:before]),
|
953
|
+
].compact
|
954
|
+
qu = _query_bool(nil, filt, nil, nil)
|
955
|
+
|
956
|
+
# the request
|
957
|
+
req = {
|
958
|
+
'query' => qu,
|
959
|
+
'aggs' => ag,
|
960
|
+
'size' => 0,
|
961
|
+
}
|
962
|
+
|
963
|
+
# run the search
|
964
|
+
url = @map[:entry] + '/_search'.freeze
|
965
|
+
body = JSON.generate(req)
|
966
|
+
head = { 'Content-Type' => 'application/json' }
|
967
|
+
resp = @es.run_request(:get, url, body, head)
|
968
|
+
raise 'search failed' if !resp.success?
|
969
|
+
|
970
|
+
# extract stats
|
971
|
+
rh = JSON.parse(resp.body)
|
972
|
+
if query[:credit]
|
973
|
+
rh = rh['aggregations']['nested']['credit']['stats']['buckets']
|
974
|
+
else
|
975
|
+
rh = rh['aggregations']['nested']['stats']['buckets']
|
976
|
+
end
|
977
|
+
list = rh.map do |hh|
|
978
|
+
{
|
979
|
+
object: {
|
980
|
+
stat: hh['key'],
|
981
|
+
sum: hh['vals']['sum'],
|
982
|
+
count: hh['vals']['count'],
|
983
|
+
min: hh['vals']['min'],
|
984
|
+
max: hh['vals']['max'],
|
985
|
+
}
|
986
|
+
}
|
987
|
+
end
|
988
|
+
|
989
|
+
# return the results
|
990
|
+
return {
|
991
|
+
query: query,
|
992
|
+
list: list
|
993
|
+
}
|
994
|
+
end # def stats()
|
995
|
+
|
996
|
+
|
997
|
+
###############################################
|
998
|
+
# constant score
|
999
|
+
#
|
1000
|
+
def _query_constant(filter)
|
1001
|
+
{'constant_score' => { 'filter' => filter } }
|
1002
|
+
end # def _query_constant()
|
1003
|
+
|
1004
|
+
|
1005
|
+
###############################################
|
1006
|
+
# (see Cache#entry_tags)
|
1007
|
+
#
|
1008
|
+
def entry_tags(query)
|
1009
|
+
|
1010
|
+
# build the query
|
1011
|
+
ag = _agg_terms('tags'.freeze, 'tags'.freeze, nil)
|
1012
|
+
qu = _query_term('caseid'.freeze, query[:caseid])
|
1013
|
+
qu = _query_constant(qu)
|
1014
|
+
req = {
|
1015
|
+
'query' => qu,
|
1016
|
+
'aggs' => ag,
|
1017
|
+
'size' => 0
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
# run the search
|
1021
|
+
url = @map[:entry] + '/_search'.freeze
|
1022
|
+
body = JSON.generate(req)
|
1023
|
+
head = { 'Content-Type' => 'application/json' }
|
1024
|
+
resp = @es.run_request(:get, url, body, head)
|
1025
|
+
raise 'search failed' if !resp.success?
|
1026
|
+
|
1027
|
+
# extract tags
|
1028
|
+
rh = JSON.parse(resp.body)
|
1029
|
+
rh = rh['aggregations']['tags']['buckets']
|
1030
|
+
list = rh.map do |hh|
|
1031
|
+
{
|
1032
|
+
object: {
|
1033
|
+
caseid: query[:caseid],
|
1034
|
+
tag: hh['key'],
|
1035
|
+
count: hh['doc_count'],
|
1036
|
+
}
|
1037
|
+
}
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
return {
|
1041
|
+
query: query,
|
1042
|
+
list: list.sort{|aa, bb| aa[:object][:tag] <=> bb[:object][:tag]}
|
1043
|
+
}
|
1044
|
+
end # def entry_tags()
|
1045
|
+
|
1046
|
+
|
1047
|
+
###############################################
|
1048
|
+
# (see Cache#case_tags)
|
1049
|
+
#
|
1050
|
+
def case_tags(query)
|
1051
|
+
|
1052
|
+
# build the query
|
1053
|
+
filter = [
|
1054
|
+
_query_term('status'.freeze, query[:status]),
|
1055
|
+
_query_term('template'.freeze, query[:template]),
|
1056
|
+
].compact
|
1057
|
+
access = [
|
1058
|
+
_query_term('access.grant'.freeze, query[:grantee]),
|
1059
|
+
_query_term('access.perm'.freeze, query[:perm]),
|
1060
|
+
].compact
|
1061
|
+
unless access.empty?
|
1062
|
+
qu = (access.size == 1) ? access[0] : _query_bool(nil, access, nil, nil)
|
1063
|
+
filter << _query_nested('access'.freeze, qu)
|
1064
|
+
end
|
1065
|
+
qu = _query_bool(nil, filter, nil, nil)
|
1066
|
+
ag = _agg_terms('tags'.freeze, 'tags'.freeze, nil)
|
1067
|
+
req = {
|
1068
|
+
'query' => qu,
|
1069
|
+
'aggs' => ag,
|
1070
|
+
'size' => 0
|
1071
|
+
}
|
1072
|
+
|
1073
|
+
# run the search
|
1074
|
+
url = @map[:case] + '/_search'.freeze
|
1075
|
+
body = JSON.generate(req)
|
1076
|
+
head = { 'Content-Type' => 'application/json' }
|
1077
|
+
resp = @es.run_request(:get, url, body, head)
|
1078
|
+
raise 'search failed' if !resp.success?
|
1079
|
+
|
1080
|
+
# extract tags
|
1081
|
+
rh = JSON.parse(resp.body)
|
1082
|
+
rh = rh['aggregations']['tags']['buckets']
|
1083
|
+
list = rh.map do |hh|
|
1084
|
+
{
|
1085
|
+
object: {
|
1086
|
+
tag: hh['key'],
|
1087
|
+
count: hh['doc_count'],
|
1088
|
+
}
|
1089
|
+
}
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
return {
|
1093
|
+
query: query,
|
1094
|
+
list: list.sort{|aa, bb| aa[:object][:tag] <=> bb[:object][:tag] }
|
1095
|
+
}
|
1096
|
+
end # def case_tags()
|
1097
|
+
|
1098
|
+
|
1099
|
+
###############################################
|
1100
|
+
# (see Cache#action_tags)
|
1101
|
+
#
|
1102
|
+
def action_tags(query)
|
1103
|
+
|
1104
|
+
# build the query
|
1105
|
+
task_filter = [
|
1106
|
+
_query_term('tasks.assigned'.freeze, query[:assigned]),
|
1107
|
+
_query_term('tasks.status'.freeze, query[:status]),
|
1108
|
+
_query_term('tasks.flag'.freeze, query[:flag]),
|
1109
|
+
_query_times('tasks.time'.freeze, query[:after], query[:before]),
|
1110
|
+
].compact
|
1111
|
+
qu_filt = _query_bool(nil, task_filter, nil, nil)
|
1112
|
+
ag = _agg_terms('tags'.freeze, 'tasks.tags'.freeze, nil)
|
1113
|
+
ag = _agg_filter('filt'.freeze, qu_filt, ag)
|
1114
|
+
ag = _agg_nested('nest'.freeze, 'tasks'.freeze, ag)
|
1115
|
+
if query[:caseid]
|
1116
|
+
qu = _query_term('caseid'.freeze, query[:caseid])
|
1117
|
+
else
|
1118
|
+
qu = _query_all()
|
1119
|
+
end
|
1120
|
+
req = {
|
1121
|
+
'query' => qu,
|
1122
|
+
'aggs' => ag,
|
1123
|
+
'size' => 0
|
1124
|
+
}
|
1125
|
+
|
1126
|
+
# run the search
|
1127
|
+
url = @map[:action] + '/_search'.freeze
|
1128
|
+
body = JSON.generate(req)
|
1129
|
+
head = { 'Content-Type' => 'application/json' }
|
1130
|
+
resp = @es.run_request(:get, url, body, head)
|
1131
|
+
raise 'search failed' if !resp.success?
|
1132
|
+
|
1133
|
+
# extract tags
|
1134
|
+
rh = JSON.parse(resp.body)
|
1135
|
+
rh = rh['aggregations']['nest']['filt']['tags']['buckets']
|
1136
|
+
list = rh.map do |hh|
|
1137
|
+
{
|
1138
|
+
object: {
|
1139
|
+
tag: hh['key'],
|
1140
|
+
count: hh['doc_count'],
|
1141
|
+
}
|
1142
|
+
}
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
return {
|
1146
|
+
query: query,
|
1147
|
+
list: list.sort{|aa, bb| aa[:object][:tag] <=> bb[:object][:tag]}
|
1148
|
+
}
|
1149
|
+
end # def action_tags()
|
1150
|
+
|
1151
|
+
|
1152
|
+
end # class ICFS::CacheElastic
|
1153
|
+
|
1154
|
+
end # module ICFS
|