2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6 * Copyright © 2017-2018 Amdocs
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
21 package org.onap.aai.sa.searchdbabstraction.searchapi;
23 import com.fasterxml.jackson.annotation.JsonProperty;
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.concurrent.atomic.AtomicBoolean;
29 * This class represents a simple term query.
32 * A term query takes an operator, a field to apply the query to and a value to match against the query contents.
35 * Valid operators include:
37 * <li>match - Field must contain the supplied value to produce a match.</li>
38 * <li>not-match - Field must NOT contain the supplied value to produce a match.</li>
40 * The following examples illustrate the structure of a few variants of the term query:
45 * // Single Field Match Query:
47 * "match": {"field": "searchTags", "value": "abcd"}
50 * // Single Field Not-Match query:
52 * "not-match": {"field": "searchTags", "value": "efgh"}
59 * // Multi Field Match Query With A Single Value:
61 * "match": {"field": "entityType searchTags", "value": "pserver"}
64 * // Multi Field Match Query With Multiple Values:
66 * "match": {"field": "entityType searchTags", "value": "pserver tenant"}
70 public class TermQuery {
73 * The name of the field to apply the term query to.
78 * The value which the field must contain in order to have a match.
83 * For multi field queries only. Determines the rules for whether or not a document matches the query, as follows:
86 * "and" - At least one occurrence of every supplied value must be present in any of the supplied fields.
89 * "or" - At least one occurrence of any of the supplied values must be present in any of the supplied fields.
91 private String operator;
93 @JsonProperty("analyzer")
94 private String searchAnalyzer;
97 public String getField() {
101 public void setField(String field) {
105 public Object getValue() {
109 public void setValue(Object value) {
113 private boolean isNumericValue() {
114 return ((value instanceof Integer) || (value instanceof Double));
117 public String getOperator() {
121 public void setOperator(String operator) {
122 this.operator = operator;
125 public String getSearchAnalyzer() {
126 return searchAnalyzer;
129 public void setSearchAnalyzer(String searchAnalyzer) {
130 this.searchAnalyzer = searchAnalyzer;
134 * This method returns a string which represents this query in syntax that is understandable by ElasticSearch and is
135 * suitable for inclusion in an ElasticSearch query string.
137 * @return - ElasticSearch syntax string.
139 public String toElasticSearch() {
141 StringBuilder sb = new StringBuilder();
145 // Are we generating a multi field query?
146 if (isMultiFieldQuery()) {
148 // For multi field queries, we have to be careful about how we handle
149 // nested fields, so check to see if any of the specified fields are
151 if (field.contains(".")) {
153 // Build the equivalent of a multi match query across one or more nested fields.
154 toElasticSearchNestedMultiMatchQuery(sb);
158 // Build a real multi match query, since we don't need to worry about nested fields.
159 toElasticSearchMultiFieldQuery(sb);
163 // Single field query.
165 // Add the necessary wrapping if this is a query against a nested field.
166 if (fieldIsNested(field)) {
167 sb.append("{\"nested\": { \"path\": \"").append(pathForNestedField(field)).append("\", \"query\": ");
171 toElasticSearchSingleFieldQuery(sb);
173 if (fieldIsNested(field)) {
180 return sb.toString();
185 * Determines whether or not the client has specified a term query with multiple fields.
187 * @return - true if the query is referencing multiple fields, false, otherwise.
189 private boolean isMultiFieldQuery() {
191 return (field.split(" ").length > 1);
196 * Constructs a single field term query in ElasticSearch syntax.
198 * @param sb - The string builder to assemble the query string with.
199 * @return - The single term query.
201 private void toElasticSearchSingleFieldQuery(StringBuilder sb) {
203 sb.append("\"term\": {\"").append(field).append("\" : ");
205 // For numeric values, don't enclose the value in quotes.
206 if (!isNumericValue()) {
207 sb.append("\"").append(value).append("\"");
217 * Constructs a multi field query in ElasticSearch syntax.
219 * @param sb - The string builder to assemble the query string with.
220 * @return - The multi field query.
222 private void toElasticSearchMultiFieldQuery(StringBuilder sb) {
224 sb.append("\"multi_match\": {");
226 sb.append("\"query\": \"").append(value).append("\", ");
227 sb.append("\"type\": \"cross_fields\",");
228 sb.append("\"fields\": [");
230 List<String> fields = Arrays.asList(field.split(" "));
231 AtomicBoolean firstField = new AtomicBoolean(true);
232 for (String f : fields) {
233 if (!firstField.compareAndSet(true, false)) {
236 sb.append("\"").append(f.trim()).append("\"");
240 sb.append("\"operator\": \"").append((operator != null) ? operator.toLowerCase() : "and").append("\"");
242 if (searchAnalyzer != null) {
243 sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
251 * Constructs the equivalent of an ElasticSearch multi match query across multiple nested fields.
254 * Since ElasticSearch doesn't really let you do that, we have to be clever and construct an equivalent query using
255 * boolean operators to produce the same result.
257 * @param sb - The string builder to use to build the query.
259 public void toElasticSearchNestedMultiMatchQuery(StringBuilder sb) {
261 // Break out our whitespace delimited list of fields and values into a actual lists.
262 List<String> fields = Arrays.asList(field.split(" "));
263 List<String> values = Arrays.asList(((String) value).split(" ")); // GDF: revisit this cast.
265 sb.append("\"bool\": {");
267 if (operator != null) {
269 if (operator.toLowerCase().equals("and")) {
270 sb.append("\"must\": [");
271 } else if (operator.toLowerCase().equals("or")) {
272 sb.append("\"should\": [");
276 sb.append("\"must\": [");
279 AtomicBoolean firstField = new AtomicBoolean(true);
280 for (String f : fields) {
282 if (!firstField.compareAndSet(true, false)) {
288 // Is this a nested field?
289 if (fieldIsNested(f)) {
291 sb.append("\"nested\": {");
292 sb.append("\"path\": \"").append(pathForNestedField(f)).append("\", ");
293 sb.append("\"query\": ");
296 sb.append("{\"bool\": {");
297 sb.append("\"should\": [");
299 AtomicBoolean firstValue = new AtomicBoolean(true);
300 for (String v : values) {
301 if (!firstValue.compareAndSet(true, false)) {
304 sb.append("{\"match\": { \"");
305 sb.append(f).append("\": {\"query\": \"").append(v).append("\"");
307 if (searchAnalyzer != null) {
308 sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
316 if (fieldIsNested(f)) {
330 public String toString() {
331 return "field: " + field + ", value: " + value + " (" + value.getClass().getName() + ")";
334 public boolean fieldIsNested(String field) {
335 return field.contains(".");
338 public String pathForNestedField(String field) {
339 int index = field.lastIndexOf('.');
340 return field.substring(0, index);