|
10 | 10 | [clojure.string :as string] |
11 | 11 | [cmr.common.concepts :as concepts] |
12 | 12 | [cmr.common.generics :as generics] |
| 13 | + [cmr.common.time-keeper :as time-keeper] |
13 | 14 | [cmr.common.util :as util] |
| 15 | + [cmr.common-app.config :as app-config] |
14 | 16 | [cmr.indexer.data.concepts.association-util :as assoc-util] |
15 | 17 | [cmr.indexer.data.concept-parser :as c-parser] |
16 | 18 | [cmr.indexer.data.concepts.generic-util :as gen-util] |
17 | 19 | [cmr.indexer.data.concepts.keyword-util :as keyword-util] |
18 | 20 | [cmr.indexer.data.elasticsearch :as esearch] |
19 | 21 | [cmr.transmit.metadata-db :as meta-db])) |
20 | 22 |
|
21 | | -(defn field->index-complex-field |
| 23 | +(defn- field->index-complex-field |
22 | 24 | "This is an example of a complex indexer which takes a list of sub fields and |
23 | 25 | combines them into one field" |
24 | 26 | [settings data] |
|
40 | 42 | {(keyword field-name) field-value |
41 | 43 | (keyword field-name-lower) field-value-lower})) |
42 | 44 |
|
43 | | -(defn field->index-simple-array-field |
44 | | - "The gets the a sub field of an array element and puts those values |
45 | | - into a list so that each value can be searched on one of two indexes: |
46 | | - the literal case, another is all lower case." |
| 45 | +(defn- field->index-complex-field-with-values-only |
| 46 | + "Complex indexer field that handles both single objects and arrays, |
| 47 | + and formats using field values only (not field names)" |
| 48 | + [settings data] |
| 49 | + (let [field-list (get settings :Field ".") |
| 50 | + field-data (get-in data (generics/jq->list field-list keyword) {}) |
| 51 | + config (get settings :Configuration {}) |
| 52 | + sub-fields (get config :sub-fields {}) |
| 53 | + layout (get config :format "%s=%s") |
| 54 | + field-name (util/safe-lowercase (:Name settings)) |
| 55 | + field-name-lower (str field-name "-lowercase") |
| 56 | + field-value (if (vector? field-data) |
| 57 | + ;; Handle array case |
| 58 | + (mapv (fn [element] |
| 59 | + (let [values (map #(get element (keyword %)) sub-fields)] |
| 60 | + (apply format layout values))) |
| 61 | + field-data) |
| 62 | + ;; Handle single object case |
| 63 | + (let [values (map #(get field-data (keyword %)) sub-fields)] |
| 64 | + (apply format layout values))) |
| 65 | + |
| 66 | + field-value-lower (if (vector? field-value) |
| 67 | + (mapv #(util/safe-lowercase %) field-value) |
| 68 | + (util/safe-lowercase field-value))] |
| 69 | + {(keyword field-name) field-value |
| 70 | + (keyword field-name-lower) field-value-lower})) |
| 71 | + |
| 72 | +(defn- field->index-simple-array-field |
| 73 | + "The gets the a sub field of an array element and puts those values into a list so that each value |
| 74 | + can be searched on one of two indexes: the literal case, another is all lower case. |
| 75 | + Example usage: |
| 76 | + (field->index-simple-array-field |
| 77 | + ;; settings |
| 78 | + {:Field \".ConceptIds\" |
| 79 | + :Name \"Concept-Ids\" |
| 80 | + :Configuration {:sub-fields [\"Value\", \"ShortName\"]}} |
| 81 | + ;; data |
| 82 | + {:ConceptIds [{:Value \"V-One\" :ShortName \"Short-One\" :ignore \"I-one\"} |
| 83 | + {:Value \"V-Two\" :ShortName \"Short-Two\" :ignore \"I-two\"}]}) |
| 84 | + Result: |
| 85 | + {:concept-ids \"Short-Two Short-One V-Two V-One\" |
| 86 | + :concept-ids-lowercase \"short-two short-one v-two v-one\"} |
| 87 | + " |
47 | 88 | [settings data] |
48 | 89 | (let [field-list (get settings :Field ".") |
49 | 90 | field-data (get-in data (generics/jq->list field-list keyword) {}) |
50 | 91 | field-name (util/safe-lowercase (:Name settings)) |
51 | 92 | field-name-lower (str field-name "-lowercase") |
52 | 93 | config (get settings :Configuration {}) |
53 | 94 | sub-fields (get config :sub-fields {}) |
54 | | - value (reduce (fn [data, key-name] |
55 | | - (into data (map (keyword key-name) field-data))) |
56 | | - (sequence nil) |
57 | | - sub-fields) |
58 | | - value-lower (map #(util/safe-lowercase %) value)] |
| 95 | + value-raw (reduce (fn [data, key-name] |
| 96 | + (into data (map (keyword key-name) field-data))) |
| 97 | + (sequence nil) |
| 98 | + sub-fields) |
| 99 | + value (cond |
| 100 | + (string? value-raw) value-raw |
| 101 | + (coll? value-raw) (clojure.string/join " " (map str value-raw)) |
| 102 | + :else (str value-raw)) |
| 103 | + value-lower (util/safe-lowercase value)] |
59 | 104 | {(keyword field-name) value |
60 | 105 | (keyword field-name-lower) value-lower})) |
61 | 106 |
|
62 | | -(defn field->index-default-field |
63 | | - "The default indexer which will map one metadata field to two indexes. One is |
64 | | - with the literal case, another is all lower case" |
| 107 | +(defn- field->index-default-field |
| 108 | + "The default indexer which will map one metadata field to two indexes. One is with the literal |
| 109 | + case, another is all lower case. If no field is found, then don't return anything |
| 110 | + Example Usage: |
| 111 | + (field->index-default-field |
| 112 | + {:Field \".VisualizationType\" :Name \"VisualizationType\"} |
| 113 | + {:VisualizationType \"Default\"}) |
| 114 | + Result: |
| 115 | + {:visualizationtype \"Default\", :visualizationtype-lowercase \"default\"} |
| 116 | + " |
65 | 117 | [settings data] |
66 | 118 | (let [field-name (util/safe-lowercase (:Name settings)) |
67 | 119 | field-name-lower (str field-name "-lowercase") |
68 | 120 | value (get-in data (generics/jq->list (:Field settings) keyword)) |
69 | 121 | value-lower (util/safe-lowercase value)] |
70 | | - {(keyword field-name) value |
71 | | - (keyword field-name-lower) value-lower})) |
| 122 | + (when value |
| 123 | + {(keyword field-name) value |
| 124 | + (keyword field-name-lower) value-lower}))) |
72 | 125 |
|
73 | | -(defn field->index |
| 126 | +(defn- field->index |
74 | 127 | "Functions which convert a part of metadata to a name-value which can be added |
75 | 128 | to an index document. This function is directed by looking for an :Indexer |
76 | 129 | value in settings and assuming :default if it is not set. |
|
86 | 139 | [settings data] |
87 | 140 | (case (:Indexer settings) |
88 | 141 | "complex-field" (field->index-complex-field settings data) |
| 142 | + "complex-fields-only" (field->index-complex-field-with-values-only settings data) |
89 | 143 | "simple-array-field" (field->index-simple-array-field settings data) |
90 | 144 | (field->index-default-field settings data))) |
91 | 145 |
|
| 146 | +(defn- merge-or-concat |
| 147 | + "Facilitate the behavior of the :AllowAppending configuration by concatinating two values" |
| 148 | + ([existing new-value] |
| 149 | + (merge-or-concat existing new-value " ")) |
| 150 | + ([existing new-value separator] |
| 151 | + (string/trim (if (nil? existing) |
| 152 | + (str new-value) |
| 153 | + (if (or (empty? existing) (empty? new-value)) |
| 154 | + (str existing new-value) |
| 155 | + (str existing separator new-value)))))) |
| 156 | + |
| 157 | +(defn- post-process-keyword |
| 158 | + "Go through the :keyword and :keyword-lowercase fields in the Elastic Document and make sure they |
| 159 | + are tokenized and sorted in the 'CMR' way." |
| 160 | + [doc] |
| 161 | + (-> doc |
| 162 | + (assoc :keyword |
| 163 | + (keyword-util/field-values->keyword-text [(:keyword doc)])) |
| 164 | + (assoc :keyword-lowercase |
| 165 | + (keyword-util/field-values->keyword-text [(:keyword-lowercase doc)])))) |
| 166 | + |
| 167 | +(defn- release-number |
| 168 | + "Format the software release number for using in the index document. Use a date if in development." |
| 169 | + [] |
| 170 | + (let [rel-ver (app-config/release-version)] |
| 171 | + (if (= rel-ver "dev") |
| 172 | + (format "%s-%s", rel-ver, (time-keeper/now)) |
| 173 | + (app-config/release-version)))) |
| 174 | + |
92 | 175 | (defn- parsed-concept->elastic-doc |
93 | 176 | "Generate elastic document" |
94 | | - [context concept parsed-concept] |
95 | | - (let [{:keys [concept-id revision-id deleted provider-id user-id |
96 | | - revision-date native-id]} concept |
97 | | - parsed-concept (if (:deleted concept) |
98 | | - (c-parser/parse-concept context concept) |
99 | | - parsed-concept) |
100 | | - generic-associations (esearch/parse-non-tombstone-associations |
101 | | - context |
102 | | - (meta-db/get-generic-associations-for-concept context concept)) |
103 | | - gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] "")) |
104 | | - gen-ver (get-in parsed-concept [:MetadataSpecification :Version]) |
105 | | - index-data-file (format "schemas/%s/v%s/config.json" gen-name gen-ver) |
106 | | - index-file-raw (slurp (io/resource index-data-file)) |
107 | | - index-data (json/parse-string index-file-raw true) |
108 | | - schema-keys [:LongName |
109 | | - :Version |
110 | | - :Description |
111 | | - :RelatedURLs] |
112 | | - keyword-values (keyword-util/concept-keys->keyword-text |
113 | | - parsed-concept schema-keys) |
114 | | - common-doc ;; fields common to all generic documents |
115 | | - {:concept-id concept-id |
116 | | - :revision-id revision-id |
117 | | - :deleted deleted |
118 | | - :gen-name gen-name |
119 | | - :gen-name-lowercase (util/safe-lowercase gen-name) |
120 | | - :gen-version gen-ver |
121 | | - :generic-type (str gen-name " " gen-ver) |
122 | | - :provider-id provider-id |
123 | | - :provider-id-lowercase (util/safe-lowercase provider-id) |
124 | | - :keyword keyword-values |
125 | | - :user-id user-id |
126 | | - :revision-date revision-date |
127 | | - :native-id native-id |
128 | | - :native-id-lowercase (string/lower-case native-id) |
129 | | - :associations-gzip-b64 (assoc-util/associations->gzip-base64-str generic-associations concept-id)} |
130 | | - configs (gen-util/only-elastic-preferences (:Indexes index-data)) |
131 | | - ;; now add the configured indexes |
132 | | - doc (reduce |
133 | | - (fn [data, config] (into data (field->index config parsed-concept))) |
134 | | - common-doc |
135 | | - configs)] |
136 | | - doc)) |
| 177 | + ([context concept parsed-concept] |
| 178 | + ;; Abstract away the need to have context or find files in the jar with this form of the function |
| 179 | + ;; so that the next interface can be called in tests. |
| 180 | + (let [parsed-concept (if (:deleted concept) |
| 181 | + (c-parser/parse-concept context concept) |
| 182 | + parsed-concept) |
| 183 | + generic-associations (esearch/parse-non-tombstone-associations |
| 184 | + context |
| 185 | + (meta-db/get-generic-associations-for-concept context concept)) |
| 186 | + gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] "")) |
| 187 | + gen-ver (get-in parsed-concept [:MetadataSpecification :Version]) |
| 188 | + index-data (-> "schemas/%s/v%s/config.json" |
| 189 | + (format gen-name gen-ver) |
| 190 | + (io/resource) |
| 191 | + (slurp) |
| 192 | + (json/parse-string true))] |
| 193 | + (parsed-concept->elastic-doc concept |
| 194 | + parsed-concept |
| 195 | + generic-associations |
| 196 | + gen-name |
| 197 | + gen-ver |
| 198 | + index-data))) |
| 199 | + |
| 200 | + ;; Creating a new overload of the function so that the core functionality can be tested without a |
| 201 | + ;; context |
| 202 | + ([concept parsed-concept generic-associations gen-name gen-ver index-data] |
| 203 | + (let [{:keys [concept-id revision-id deleted provider-id user-id |
| 204 | + revision-date native-id]} concept |
| 205 | + allow-appending (get-in index-data [:IndexConfiguration :AllowAppending]) |
| 206 | + additional-keywords (get-in index-data [:IndexConfiguration :AdditionalKeywords] {}) |
| 207 | + ;; Allow Generic Document Configurations to extend the list of simple top level fields that |
| 208 | + ;; can be added to the keyword field. This process does not preclude the use of more |
| 209 | + ;; complicated fields also being added when using the "AllowAppending" setting. |
| 210 | + schema-keys (into [:LongName |
| 211 | + :Version |
| 212 | + :Description |
| 213 | + :RelatedURLs] |
| 214 | + (map keyword additional-keywords)) |
| 215 | + keyword-values (keyword-util/concept-keys->keyword-text |
| 216 | + parsed-concept schema-keys) |
| 217 | + common-doc ;; fields common to all generic documents |
| 218 | + {:cmr-version (release-number) |
| 219 | + :concept-id concept-id |
| 220 | + :revision-id revision-id |
| 221 | + :deleted deleted |
| 222 | + :gen-name gen-name |
| 223 | + :gen-name-lowercase (util/safe-lowercase gen-name) |
| 224 | + :gen-version gen-ver |
| 225 | + :generic-type (str gen-name " " gen-ver) |
| 226 | + :provider-id provider-id |
| 227 | + :provider-id-lowercase (util/safe-lowercase provider-id) |
| 228 | + :keyword keyword-values |
| 229 | + :keyword-lowercase keyword-values |
| 230 | + :user-id user-id |
| 231 | + :revision-date revision-date |
| 232 | + :native-id native-id |
| 233 | + :native-id-lowercase (string/lower-case native-id) |
| 234 | + :associations-gzip-b64 (assoc-util/associations->gzip-base64-str |
| 235 | + generic-associations concept-id)} |
| 236 | + configs (gen-util/only-elastic-preferences (:Indexes index-data)) |
| 237 | + ;; now add the configured indexes |
| 238 | + doc (reduce |
| 239 | + (fn [data config] |
| 240 | + ;; Allow Appending from the configuration file allows for multiple rules to be |
| 241 | + ;; defined using the same :Name. When this is set, then these indexes are added |
| 242 | + ;; together to form one larger value made up of different parts of the Generic |
| 243 | + ;; document. Otherwise, the legacy behavier is that the last rule 'wins'. |
| 244 | + (if allow-appending |
| 245 | + (merge-with merge-or-concat data (field->index config parsed-concept)) |
| 246 | + (into data (field->index config parsed-concept)))) |
| 247 | + common-doc |
| 248 | + configs)] |
| 249 | + ;; Finally clean up the keyword indexs which may have been appended |
| 250 | + (post-process-keyword doc)))) |
137 | 251 |
|
138 | 252 | (defn field->keyword-fields |
139 | 253 | "Gets the name of the fields that are used for keyword searches. |
|
170 | 284 | parsed-concept (if (:deleted concept) |
171 | 285 | (c-parser/parse-concept context concept) |
172 | 286 | parsed-concept) |
| 287 | + ;; No associations in drafts |
173 | 288 | concept-type (concepts/concept-id->type (:concept-id concept)) |
174 | 289 | version (generics/current-generic-version concept-type) |
175 | 290 | gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] "")) |
176 | 291 | gen-ver (get-in parsed-concept [:MetadataSpecification :Version]) |
177 | 292 | index-data-file (format "schemas/%s/v%s/config.json" (name concept-type) version) |
178 | 293 | index-file-raw (slurp (io/resource index-data-file)) |
179 | 294 | index-data (json/parse-string index-file-raw true) |
| 295 | + ;; No default keyword processing till latter |
180 | 296 | common-doc ;; fields common to all generic documents |
181 | | - {:concept-id concept-id |
| 297 | + {:cmr-version (release-number) |
| 298 | + :concept-id concept-id |
182 | 299 | :revision-id revision-id |
183 | 300 | :deleted deleted |
184 | 301 | :gen-name gen-name |
|
0 commit comments