Initial search service commit
[aai/search-data-service.git] / src / main / java / org / openecomp / sa / searchdbabstraction / searchapi / TermQuery.java
1 /**
2  * ============LICENSE_START=======================================================
3  * Search Data Service
4  * ================================================================================
5  * Copyright © 2017 AT&T Intellectual Property.
6  * Copyright © 2017 Amdocs
7  * All rights reserved.
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License ati
12  *
13  *    http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  *
22  * ECOMP and OpenECOMP are trademarks
23  * and service marks of AT&T Intellectual Property.
24  */
25 package org.openecomp.sa.searchdbabstraction.searchapi;
26
27 import com.fasterxml.jackson.annotation.JsonProperty;
28 import edu.emory.mathcs.backport.java.util.Arrays;
29
30 import java.util.List;
31 import java.util.concurrent.atomic.AtomicBoolean;
32
33 /**
34  * This class represents a simple term query.
35  *
36  * <p>A term query takes an operator, a field to apply the query to and a value to match
37  * against the query contents.
38  *
39  * <p>Valid operators include:
40  * <ul>
41  * <li> match - Field must contain the supplied value to produce a match. </li>
42  * <li> not-match - Field must NOT contain the supplied value to produce a match. </li>
43  * </ul>
44  * The following examples illustrate the structure of a few variants of the
45  * term query:
46  *
47  * <p><pre>
48  *     // Single Field Match Query:
49  *     {
50  *         "match": {"field": "searchTags", "value": "abcd"}
51  *     }
52  *
53  *     // Single Field Not-Match query:
54  *     {
55  *         "not-match": {"field": "searchTags", "value": "efgh"}
56  *     }
57  * </pre>
58  *
59  * <p><pre>
60  *     // Multi Field Match Query With A Single Value:
61  *     {
62  *         "match": {"field": "entityType searchTags", "value": "pserver"}
63  *     }
64  *
65  *     // Multi Field Match Query With Multiple Values:
66  *     {
67  *         "match": {"field": "entityType searchTags", "value": "pserver tenant"}
68  *     }
69  * </pre>
70  */
71 public class TermQuery {
72
73   /**
74    * The name of the field to apply the term query to.
75    */
76   private String field;
77
78   /**
79    * The value which the field must contain in order to have a match.
80    */
81   private Object value;
82
83   /**
84    * For multi field queries only.  Determines the rules for whether or not a document matches
85    * the query, as follows:
86    *
87    * <p>"and" - At least one occurrence of every supplied value must be present in any of the
88    * supplied fields.
89    *
90    * <p>"or"  - At least one occurrence of any of the supplied values must be present in any of
91    * the supplied fields.
92    */
93   private String operator;
94
95   @JsonProperty("analyzer")
96   private String searchAnalyzer;
97
98
99   public String getField() {
100     return field;
101   }
102
103   public void setField(String field) {
104     this.field = field;
105   }
106
107   public Object getValue() {
108     return value;
109   }
110
111   public void setValue(Object value) {
112     this.value = value;
113   }
114
115   private boolean isNumericValue() {
116     return ((value instanceof Integer) || (value instanceof Double));
117   }
118
119   public String getOperator() {
120     return operator;
121   }
122
123   public void setOperator(String operator) {
124     this.operator = operator;
125   }
126
127   public String getSearchAnalyzer() {
128     return searchAnalyzer;
129   }
130
131   public void setSearchAnalyzer(String searchAnalyzer) {
132     this.searchAnalyzer = searchAnalyzer;
133   }
134
135   /**
136    * This method returns a string which represents this query in syntax
137    * that is understandable by ElasticSearch and is suitable for inclusion
138    * in an ElasticSearch query string.
139    *
140    * @return - ElasticSearch syntax string.
141    */
142   public String toElasticSearch() {
143
144     StringBuilder sb = new StringBuilder();
145
146     sb.append("{");
147
148     // Are we generating a multi field query?
149     if (isMultiFieldQuery()) {
150
151       // For multi field queries, we have to be careful about how we handle
152       // nested fields, so check to see if any of the specified fields are
153       // nested.
154       if (field.contains(".")) {
155
156         // Build the equivalent of a multi match query across one or more nested fields.
157         toElasticSearchNestedMultiMatchQuery(sb);
158
159       } else {
160
161         // Build a real multi match query, since we don't need to worry about nested fields.
162         toElasticSearchMultiFieldQuery(sb);
163       }
164     } else {
165
166       // Single field query.
167
168       // Add the necessary wrapping if this is a query against a nested field.
169       if (fieldIsNested(field)) {
170         sb.append("{\"nested\": { \"path\": \"").append(pathForNestedField(field))
171             .append("\", \"query\": ");
172       }
173
174       // Build the query.
175       toElasticSearchSingleFieldQuery(sb);
176
177       if (fieldIsNested(field)) {
178         sb.append("}}");
179       }
180     }
181
182     sb.append("}");
183
184     return sb.toString();
185   }
186
187
188   /**
189    * Determines whether or not the client has specified a term query with
190    * multiple fields.
191    *
192    * @return - true if the query is referencing multiple fields, false, otherwise.
193    */
194   private boolean isMultiFieldQuery() {
195
196     return (field.split(" ").length > 1);
197   }
198
199
200   /**
201    * Constructs a single field term query in ElasticSearch syntax.
202    *
203    * @param sb - The string builder to assemble the query string with.
204    * @return - The single term query.
205    */
206   private void toElasticSearchSingleFieldQuery(StringBuilder sb) {
207
208     sb.append("\"term\": {\"").append(field).append("\" : ");
209
210     // For numeric values, don't enclose the value in quotes.
211     if (!isNumericValue()) {
212       sb.append("\"").append(value).append("\"");
213     } else {
214       sb.append(value);
215     }
216
217     sb.append("}");
218   }
219
220
221   /**
222    * Constructs a multi field query in ElasticSearch syntax.
223    *
224    * @param sb - The string builder to assemble the query string with.
225    * @return - The multi field query.
226    */
227   private void toElasticSearchMultiFieldQuery(StringBuilder sb) {
228
229     sb.append("\"multi_match\": {");
230
231     sb.append("\"query\": \"").append(value).append("\", ");
232     sb.append("\"type\": \"cross_fields\",");
233     sb.append("\"fields\": [");
234
235     List<String> fields = Arrays.asList(field.split(" "));
236     AtomicBoolean firstField = new AtomicBoolean(true);
237     for (String f : fields) {
238       if (!firstField.compareAndSet(true, false)) {
239         sb.append(", ");
240       }
241       sb.append("\"").append(f.trim()).append("\"");
242     }
243     sb.append("],");
244
245     sb.append("\"operator\": \"").append((operator != null)
246         ? operator.toLowerCase() : "and").append("\"");
247
248     if (searchAnalyzer != null) {
249       sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
250     }
251
252     sb.append("}");
253   }
254
255
256   /**
257    * Constructs the equivalent of an ElasticSearch multi match query across
258    * multiple nested fields.
259    *
260    * <p>Since ElasticSearch doesn't really let you do that, we have to be clever
261    * and construct an equivalent query using boolean operators to produce
262    * the same result.
263    *
264    * @param sb - The string builder to use to build the query.
265    */
266   public void toElasticSearchNestedMultiMatchQuery(StringBuilder sb) {
267
268     // Break out our whitespace delimited list of fields and values into a actual lists.
269     List<String> fields = Arrays.asList(field.split(" "));
270     List<String> values = Arrays.asList(((String) value).split(" ")); // GDF: revisit this cast.
271
272     sb.append("\"bool\": {");
273
274     if (operator != null) {
275
276       if (operator.toLowerCase().equals("and")) {
277         sb.append("\"must\": [");
278       } else if (operator.toLowerCase().equals("or")) {
279         sb.append("\"should\": [");
280       }
281
282     } else {
283       sb.append("\"must\": [");
284     }
285
286     AtomicBoolean firstField = new AtomicBoolean(true);
287     for (String f : fields) {
288
289       if (!firstField.compareAndSet(true, false)) {
290         sb.append(", ");
291       }
292
293       sb.append("{ ");
294
295       // Is this a nested field?
296       if (fieldIsNested(f)) {
297
298         sb.append("\"nested\": {");
299         sb.append("\"path\": \"").append(pathForNestedField(f)).append("\", ");
300         sb.append("\"query\": ");
301       }
302
303       sb.append("{\"bool\": {");
304       sb.append("\"should\": [");
305
306       AtomicBoolean firstValue = new AtomicBoolean(true);
307       for (String v : values) {
308         if (!firstValue.compareAndSet(true, false)) {
309           sb.append(", ");
310         }
311         sb.append("{\"match\": { \"");
312         sb.append(f).append("\": {\"query\": \"").append(v).append("\"");
313
314         if (searchAnalyzer != null) {
315           sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
316         }
317         sb.append("}}}");
318       }
319
320       sb.append("]");
321       sb.append("}");
322
323       if (fieldIsNested(f)) {
324         sb.append("}");
325         sb.append("}");
326       }
327
328       sb.append("}");
329     }
330
331     sb.append("]");
332     sb.append("}");
333   }
334
335
336   @Override
337   public String toString() {
338     return "field: " + field + ", value: " + value + " (" + value.getClass().getName() + ")";
339   }
340
341   public boolean fieldIsNested(String field) {
342     return field.contains(".");
343   }
344
345   public String pathForNestedField(String field) {
346     int index = field.lastIndexOf('.');
347     return field.substring(0, index);
348   }
349 }