Skip to content

Commit bd455e7

Browse files
authored
CMR-10529 visualisation search improvments (#2263)
* Revert "CMR-10533: Bumping up elastic to a recomended point release (#2246)" * CMR-10529: Visualization indexing improvments, fixing Keyword search to correctly include data from multiple fields by adding two new features to Generics, you can append keywords and fields with the same name
1 parent edb51d2 commit bd455e7

14 files changed

Lines changed: 590 additions & 128 deletions

File tree

Generics.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ Generic documents are documents which conform to the "Generic" API in CMR. These
55

66
## Configuration
77

8+
### Generic Configuration File
9+
10+
Within the Generic config.json file there is a section called `IndexConfiguration`. This contains two settings:
11+
12+
* AllowAppending: When set to true, then multiple Indexes with the same `Name` value will be appended together to create one larger value. Otherwise the last one in the config file will be the setting used.
13+
* AdditionalKeywords: List of simple fields to be added to the `keyword` field for general searching. By default CMR uses: LongName, Version, Description, RelatedURLs.
14+
15+
Indexes:
16+
* Description: Human readable description of field. Shows up in some logs
17+
* Field: [jq](https://jqlang.org) like path to field data
18+
* Name: Field Name
19+
* Mapping: The Elastic field type
20+
* token: text-field-mapping
21+
* string: string-field-mapping
22+
* int: int-field-mapping
23+
* date: date-field-mapping
24+
* Indexer:
25+
* default (none): direct, one-to-one mapping
26+
* simple-array-field: index a sub field of an array element
27+
* complex-fields-only: Complex indexer field that handles both single objects and arrays, and formats using field values only (not field names)
28+
* complex-field: takes a list of sub fields and combines them
29+
30+
### CMR settings
31+
832
If adding a new document, you will need to update the defconf variable by either setting an ENV for global change, or by updating the default value in [/common-lib/src/cmr/common/config.clj](/common-lib/src/cmr/common/config.clj). The format for this value is either JSON for an ENV variable or a clojure map if setting directly in the default attribute of the defconfig like this:
933

1034
(defconfig approved-pipeline-documents
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(ns cmr.elastic-utils.generics
2+
"A set of functions for dealing with Generics and Elastic")
3+
4+
(defn only-elastic-preferences
5+
"Go through all the index configurations and return only the ones related to
6+
generating elastic values. If an index does not specify what type it is for,
7+
then assume elastic"
8+
[list-of-indexes]
9+
(keep #(if (not (nil? %)) %)
10+
(map
11+
(fn [x] (when (or (nil? (:Type x)) (= "elastic" (:Type x))) x))
12+
list-of-indexes)))

indexer-app/src/cmr/indexer/data/concepts/generic.clj

Lines changed: 151 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@
99
[clojure.string :as string]
1010
[cmr.common.concepts :as concepts]
1111
[cmr.common.generics :as generics]
12+
[cmr.common.time-keeper :as time-keeper]
1213
[cmr.common.util :as util]
14+
[cmr.common-app.config :as app-config]
1315
[cmr.indexer.data.concepts.association-util :as assoc-util]
1416
[cmr.indexer.data.concept-parser :as c-parser]
1517
[cmr.indexer.data.concepts.generic-util :as gen-util]
1618
[cmr.indexer.data.concepts.keyword-util :as keyword-util]
1719
[cmr.indexer.data.elasticsearch :as esearch]
1820
[cmr.transmit.metadata-db :as meta-db]))
1921

20-
(defn field->index-complex-field
22+
(defn- field->index-complex-field
2123
"This is an example of a complex indexer which takes a list of sub fields and
2224
combines them into one field"
2325
[settings data]
@@ -39,7 +41,7 @@
3941
{(keyword field-name) field-value
4042
(keyword field-name-lower) field-value-lower}))
4143

42-
(defn field->index-complex-field-with-values-only
44+
(defn- field->index-complex-field-with-values-only
4345
"Complex indexer field that handles both single objects and arrays,
4446
and formats using field values only (not field names)"
4547
[settings data]
@@ -56,7 +58,7 @@
5658
(let [values (map #(get element (keyword %)) sub-fields)]
5759
(apply format layout values)))
5860
field-data)
59-
;; Handle single object case
61+
;; Handle single object case
6062
(let [values (map #(get field-data (keyword %)) sub-fields)]
6163
(apply format layout values)))
6264

@@ -66,37 +68,61 @@
6668
{(keyword field-name) field-value
6769
(keyword field-name-lower) field-value-lower}))
6870

69-
(defn field->index-simple-array-field
70-
"The gets the a sub field of an array element and puts those values
71-
into a list so that each value can be searched on one of two indexes:
72-
the literal case, another is all lower case."
71+
(defn- field->index-simple-array-field
72+
"The gets the a sub field of an array element and puts those values into a list so that each value
73+
can be searched on one of two indexes: the literal case, another is all lower case.
74+
Example usage:
75+
(field->index-simple-array-field
76+
;; settings
77+
{:Field \".ConceptIds\"
78+
:Name \"Concept-Ids\"
79+
:Configuration {:sub-fields [\"Value\", \"ShortName\"]}}
80+
;; data
81+
{:ConceptIds [{:Value \"V-One\" :ShortName \"Short-One\" :ignore \"I-one\"}
82+
{:Value \"V-Two\" :ShortName \"Short-Two\" :ignore \"I-two\"}]})
83+
Result:
84+
{:concept-ids \"Short-Two Short-One V-Two V-One\"
85+
:concept-ids-lowercase \"short-two short-one v-two v-one\"}
86+
"
7387
[settings data]
7488
(let [field-list (get settings :Field ".")
7589
field-data (get-in data (generics/jq->list field-list keyword) {})
7690
field-name (util/safe-lowercase (:Name settings))
7791
field-name-lower (str field-name "-lowercase")
7892
config (get settings :Configuration {})
7993
sub-fields (get config :sub-fields {})
80-
value (reduce (fn [data, key-name]
81-
(into data (map (keyword key-name) field-data)))
82-
(sequence nil)
83-
sub-fields)
84-
value-lower (map #(util/safe-lowercase %) value)]
94+
value-raw (reduce (fn [data, key-name]
95+
(into data (map (keyword key-name) field-data)))
96+
(sequence nil)
97+
sub-fields)
98+
value (cond
99+
(string? value-raw) value-raw
100+
(coll? value-raw) (clojure.string/join " " (map str value-raw))
101+
:else (str value-raw))
102+
value-lower (util/safe-lowercase value)]
85103
{(keyword field-name) value
86104
(keyword field-name-lower) value-lower}))
87105

88-
(defn field->index-default-field
89-
"The default indexer which will map one metadata field to two indexes. One is
90-
with the literal case, another is all lower case"
106+
(defn- field->index-default-field
107+
"The default indexer which will map one metadata field to two indexes. One is with the literal
108+
case, another is all lower case. If no field is found, then don't return anything
109+
Example Usage:
110+
(field->index-default-field
111+
{:Field \".VisualizationType\" :Name \"VisualizationType\"}
112+
{:VisualizationType \"Default\"})
113+
Result:
114+
{:visualizationtype \"Default\", :visualizationtype-lowercase \"default\"}
115+
"
91116
[settings data]
92117
(let [field-name (util/safe-lowercase (:Name settings))
93118
field-name-lower (str field-name "-lowercase")
94119
value (get-in data (generics/jq->list (:Field settings) keyword))
95120
value-lower (util/safe-lowercase value)]
96-
{(keyword field-name) value
97-
(keyword field-name-lower) value-lower}))
121+
(when value
122+
{(keyword field-name) value
123+
(keyword field-name-lower) value-lower})))
98124

99-
(defn field->index
125+
(defn- field->index
100126
"Functions which convert a part of metadata to a name-value which can be added
101127
to an index document. This function is directed by looking for an :Indexer
102128
value in settings and assuming :default if it is not set.
@@ -116,51 +142,111 @@
116142
"simple-array-field" (field->index-simple-array-field settings data)
117143
(field->index-default-field settings data)))
118144

145+
(defn- merge-or-concat
146+
"Facilitate the behavior of the :AllowAppending configuration by concatinating two values"
147+
([existing new-value]
148+
(merge-or-concat existing new-value " "))
149+
([existing new-value separator]
150+
(string/trim (if (nil? existing)
151+
(str new-value)
152+
(if (or (empty? existing) (empty? new-value))
153+
(str existing new-value)
154+
(str existing separator new-value))))))
155+
156+
(defn- post-process-keyword
157+
"Go through the :keyword and :keyword-lowercase fields in the Elastic Document and make sure they
158+
are tokenized and sorted in the 'CMR' way."
159+
[doc]
160+
(-> doc
161+
(assoc :keyword
162+
(keyword-util/field-values->keyword-text [(:keyword doc)]))
163+
(assoc :keyword-lowercase
164+
(keyword-util/field-values->keyword-text [(:keyword-lowercase doc)]))))
165+
166+
(defn- release-number
167+
"Format the software release number for using in the index document. Use a date if in development."
168+
[]
169+
(let [rel-ver (app-config/release-version)]
170+
(if (= rel-ver "dev")
171+
(format "%s-%s", rel-ver, (time-keeper/now))
172+
(app-config/release-version))))
173+
119174
(defn- parsed-concept->elastic-doc
120175
"Generate elastic document"
121-
[context concept parsed-concept]
122-
(let [{:keys [concept-id revision-id deleted provider-id user-id
123-
revision-date native-id]} concept
124-
parsed-concept (if (:deleted concept)
125-
(c-parser/parse-concept context concept)
126-
parsed-concept)
127-
generic-associations (esearch/parse-non-tombstone-associations
128-
context
129-
(meta-db/get-generic-associations-for-concept context concept))
130-
gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] ""))
131-
gen-ver (get-in parsed-concept [:MetadataSpecification :Version])
132-
index-data-file (format "schemas/%s/v%s/config.json" gen-name gen-ver)
133-
index-file-raw (slurp (io/resource index-data-file))
134-
index-data (json/parse-string index-file-raw true)
135-
schema-keys [:LongName
136-
:Version
137-
:Description
138-
:RelatedURLs]
139-
keyword-values (keyword-util/concept-keys->keyword-text
140-
parsed-concept schema-keys)
141-
common-doc ;; fields common to all generic documents
142-
{:concept-id concept-id
143-
:revision-id revision-id
144-
:deleted deleted
145-
:gen-name gen-name
146-
:gen-name-lowercase (util/safe-lowercase gen-name)
147-
:gen-version gen-ver
148-
:generic-type (str gen-name " " gen-ver)
149-
:provider-id provider-id
150-
:provider-id-lowercase (util/safe-lowercase provider-id)
151-
:keyword keyword-values
152-
:user-id user-id
153-
:revision-date revision-date
154-
:native-id native-id
155-
:native-id-lowercase (string/lower-case native-id)
156-
:associations-gzip-b64 (assoc-util/associations->gzip-base64-str generic-associations concept-id)}
157-
configs (gen-util/only-elastic-preferences (:Indexes index-data))
158-
;; now add the configured indexes
159-
doc (reduce
160-
(fn [data, config] (into data (field->index config parsed-concept)))
161-
common-doc
162-
configs)]
163-
doc))
176+
([context concept parsed-concept]
177+
;; Abstract away the need to have context or find files in the jar with this form of the function
178+
;; so that the next interface can be called in tests.
179+
(let [parsed-concept (if (:deleted concept)
180+
(c-parser/parse-concept context concept)
181+
parsed-concept)
182+
generic-associations (esearch/parse-non-tombstone-associations
183+
context
184+
(meta-db/get-generic-associations-for-concept context concept))
185+
gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] ""))
186+
gen-ver (get-in parsed-concept [:MetadataSpecification :Version])
187+
index-data (-> "schemas/%s/v%s/config.json"
188+
(format gen-name gen-ver)
189+
(io/resource)
190+
(slurp)
191+
(json/parse-string true))]
192+
(parsed-concept->elastic-doc concept
193+
parsed-concept
194+
generic-associations
195+
gen-name
196+
gen-ver
197+
index-data)))
198+
199+
;; Creating a new overload of the function so that the core functionality can be tested without a
200+
;; context
201+
([concept parsed-concept generic-associations gen-name gen-ver index-data]
202+
(let [{:keys [concept-id revision-id deleted provider-id user-id
203+
revision-date native-id]} concept
204+
allow-appending (get-in index-data [:IndexConfiguration :AllowAppending])
205+
additional-keywords (get-in index-data [:IndexConfiguration :AdditionalKeywords] {})
206+
;; Allow Generic Document Configurations to extend the list of simple top level fields that
207+
;; can be added to the keyword field. This process does not preclude the use of more
208+
;; complicated fields also being added when using the "AllowAppending" setting.
209+
schema-keys (into [:LongName
210+
:Version
211+
:Description
212+
:RelatedURLs]
213+
(map keyword additional-keywords))
214+
keyword-values (keyword-util/concept-keys->keyword-text
215+
parsed-concept schema-keys)
216+
common-doc ;; fields common to all generic documents
217+
{:cmr-version (release-number)
218+
:concept-id concept-id
219+
:revision-id revision-id
220+
:deleted deleted
221+
:gen-name gen-name
222+
:gen-name-lowercase (util/safe-lowercase gen-name)
223+
:gen-version gen-ver
224+
:generic-type (str gen-name " " gen-ver)
225+
:provider-id provider-id
226+
:provider-id-lowercase (util/safe-lowercase provider-id)
227+
:keyword keyword-values
228+
:keyword-lowercase keyword-values
229+
:user-id user-id
230+
:revision-date revision-date
231+
:native-id native-id
232+
:native-id-lowercase (string/lower-case native-id)
233+
:associations-gzip-b64 (assoc-util/associations->gzip-base64-str
234+
generic-associations concept-id)}
235+
configs (gen-util/only-elastic-preferences (:Indexes index-data))
236+
;; now add the configured indexes
237+
doc (reduce
238+
(fn [data config]
239+
;; Allow Appending from the configuration file allows for multiple rules to be
240+
;; defined using the same :Name. When this is set, then these indexes are added
241+
;; together to form one larger value made up of different parts of the Generic
242+
;; document. Otherwise, the legacy behavier is that the last rule 'wins'.
243+
(if allow-appending
244+
(merge-with merge-or-concat data (field->index config parsed-concept))
245+
(into data (field->index config parsed-concept))))
246+
common-doc
247+
configs)]
248+
;; Finally clean up the keyword indexs which may have been appended
249+
(post-process-keyword doc))))
164250

165251
(defn field->keyword-fields
166252
"Gets the name of the fields that are used for keyword searches.
@@ -197,15 +283,18 @@
197283
parsed-concept (if (:deleted concept)
198284
(c-parser/parse-concept context concept)
199285
parsed-concept)
286+
;; No associations in drafts
200287
concept-type (concepts/concept-id->type (:concept-id concept))
201288
version (generics/current-generic-version concept-type)
202289
gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] ""))
203290
gen-ver (get-in parsed-concept [:MetadataSpecification :Version])
204291
index-data-file (format "schemas/%s/v%s/config.json" (name concept-type) version)
205292
index-file-raw (slurp (io/resource index-data-file))
206293
index-data (json/parse-string index-file-raw true)
294+
;; No default keyword processing till latter
207295
common-doc ;; fields common to all generic documents
208-
{:concept-id concept-id
296+
{:cmr-version (release-number)
297+
:concept-id concept-id
209298
:revision-id revision-id
210299
:deleted deleted
211300
:gen-name gen-name
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
(ns cmr.indexer.data.concepts.generic-util
22
"Contains functions to parse and convert Generic Documents (that is a document
33
complying to a schema supported by the Generic Document system) to and object
4-
that can be indexed in lucine."
5-
(:require
6-
[clojure.string :as string]))
4+
that can be indexed in lucine.")
75

86
(defn only-elastic-preferences
97
"Go through all the index configurations and return only the ones related to
108
generating elastic values. If an index does not specify what type it is for,
119
then assume elastic"
1210
[list-of-indexes]
13-
(keep #(if (not (nil? %)) %)
11+
(keep #(when (not (nil? %)) %)
1412
(map
1513
(fn [x] (when (or (nil? (:Type x)) (= "elastic" (:Type x))) x))
1614
list-of-indexes)))

indexer-app/src/cmr/indexer/data/elasticsearch.clj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[clj-http.client :as client]
44
[cmr.common.concepts :as cs]
55
[cmr.common.lifecycle :as lifecycle]
6-
[cmr.common.log :as log :refer [info warn error]]
6+
[cmr.common.log :as log :refer [info infof warn error]]
77
[cmr.common.services.errors :as errors]
88
[cmr.common.util :as util]
99
[cmr.elastic-utils.connect :as es]
@@ -328,12 +328,12 @@
328328
(when (:error result)
329329
(if (= 409 (:status result))
330330
(if ignore-conflict?
331-
(info (str "Ignore conflict: " (str result)))
331+
(infof "Ignore conflict: %s" (str result))
332332
(errors/throw-service-error
333333
:conflict
334-
(str "Save to Elasticsearch failed " (str result))))
334+
(format "Save to Elasticsearch failed %s" (str result))))
335335
(errors/internal-error!
336-
(str "Save to Elasticsearch failed " (str result))))))))
336+
(format "Save to Elasticsearch failed %s" (str result))))))))
337337

338338
(defn get-document
339339
"Get the document from Elasticsearch, raise error if failed."
@@ -365,6 +365,6 @@
365365
(when-not (some #{200 404} [status])
366366
(if (= 409 status)
367367
(if ignore-conflict?
368-
(info (str "Ignore conflict: " (str response)))
368+
(infof "Ignore conflict: %s" (str response))
369369
(errors/throw-service-error :conflict (str "Delete from Elasticsearch failed " (str response))))
370370
(errors/internal-error! (str "Delete from Elasticsearch failed " (str response)))))))))

indexer-app/src/cmr/indexer/data/index_set_elasticsearch.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
:client-id t-config/cmr-client-id}
114114
:throw-exceptions false})
115115
status (:status response)]
116-
(if-not (some #{200 202 204} [status])
116+
(when-not (some #{200 202 204} [status])
117117
(errors/internal-error! (m/index-delete-failure-msg response))))))
118118

119119
(defn save-document-in-elastic

0 commit comments

Comments
 (0)