|
9 | 9 | [clojure.string :as string] |
10 | 10 | [cmr.common.concepts :as concepts] |
11 | 11 | [cmr.common.generics :as generics] |
| 12 | + [cmr.common.time-keeper :as time-keeper] |
12 | 13 | [cmr.common.util :as util] |
| 14 | + [cmr.common-app.config :as app-config] |
13 | 15 | [cmr.indexer.data.concepts.association-util :as assoc-util] |
14 | 16 | [cmr.indexer.data.concept-parser :as c-parser] |
15 | 17 | [cmr.indexer.data.concepts.generic-util :as gen-util] |
16 | 18 | [cmr.indexer.data.concepts.keyword-util :as keyword-util] |
17 | 19 | [cmr.indexer.data.elasticsearch :as esearch] |
18 | 20 | [cmr.transmit.metadata-db :as meta-db])) |
19 | 21 |
|
20 | | -(defn field->index-complex-field |
| 22 | +(defn- field->index-complex-field |
21 | 23 | "This is an example of a complex indexer which takes a list of sub fields and |
22 | 24 | combines them into one field" |
23 | 25 | [settings data] |
|
39 | 41 | {(keyword field-name) field-value |
40 | 42 | (keyword field-name-lower) field-value-lower})) |
41 | 43 |
|
42 | | -(defn field->index-complex-field-with-values-only |
| 44 | +(defn- field->index-complex-field-with-values-only |
43 | 45 | "Complex indexer field that handles both single objects and arrays, |
44 | 46 | and formats using field values only (not field names)" |
45 | 47 | [settings data] |
|
56 | 58 | (let [values (map #(get element (keyword %)) sub-fields)] |
57 | 59 | (apply format layout values))) |
58 | 60 | field-data) |
59 | | - ;; Handle single object case |
| 61 | + ;; Handle single object case |
60 | 62 | (let [values (map #(get field-data (keyword %)) sub-fields)] |
61 | 63 | (apply format layout values))) |
62 | 64 |
|
|
66 | 68 | {(keyword field-name) field-value |
67 | 69 | (keyword field-name-lower) field-value-lower})) |
68 | 70 |
|
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 | + " |
73 | 87 | [settings data] |
74 | 88 | (let [field-list (get settings :Field ".") |
75 | 89 | field-data (get-in data (generics/jq->list field-list keyword) {}) |
76 | 90 | field-name (util/safe-lowercase (:Name settings)) |
77 | 91 | field-name-lower (str field-name "-lowercase") |
78 | 92 | config (get settings :Configuration {}) |
79 | 93 | 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)] |
85 | 103 | {(keyword field-name) value |
86 | 104 | (keyword field-name-lower) value-lower})) |
87 | 105 |
|
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 | + " |
91 | 116 | [settings data] |
92 | 117 | (let [field-name (util/safe-lowercase (:Name settings)) |
93 | 118 | field-name-lower (str field-name "-lowercase") |
94 | 119 | value (get-in data (generics/jq->list (:Field settings) keyword)) |
95 | 120 | 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}))) |
98 | 124 |
|
99 | | -(defn field->index |
| 125 | +(defn- field->index |
100 | 126 | "Functions which convert a part of metadata to a name-value which can be added |
101 | 127 | to an index document. This function is directed by looking for an :Indexer |
102 | 128 | value in settings and assuming :default if it is not set. |
|
116 | 142 | "simple-array-field" (field->index-simple-array-field settings data) |
117 | 143 | (field->index-default-field settings data))) |
118 | 144 |
|
| 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 | + |
119 | 174 | (defn- parsed-concept->elastic-doc |
120 | 175 | "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)))) |
164 | 250 |
|
165 | 251 | (defn field->keyword-fields |
166 | 252 | "Gets the name of the fields that are used for keyword searches. |
|
197 | 283 | parsed-concept (if (:deleted concept) |
198 | 284 | (c-parser/parse-concept context concept) |
199 | 285 | parsed-concept) |
| 286 | + ;; No associations in drafts |
200 | 287 | concept-type (concepts/concept-id->type (:concept-id concept)) |
201 | 288 | version (generics/current-generic-version concept-type) |
202 | 289 | gen-name (csk/->kebab-case (get-in parsed-concept [:MetadataSpecification :Name] "")) |
203 | 290 | gen-ver (get-in parsed-concept [:MetadataSpecification :Version]) |
204 | 291 | index-data-file (format "schemas/%s/v%s/config.json" (name concept-type) version) |
205 | 292 | index-file-raw (slurp (io/resource index-data-file)) |
206 | 293 | index-data (json/parse-string index-file-raw true) |
| 294 | + ;; No default keyword processing till latter |
207 | 295 | common-doc ;; fields common to all generic documents |
208 | | - {:concept-id concept-id |
| 296 | + {:cmr-version (release-number) |
| 297 | + :concept-id concept-id |
209 | 298 | :revision-id revision-id |
210 | 299 | :deleted deleted |
211 | 300 | :gen-name gen-name |
|
0 commit comments