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.
31 * <p>A term query takes an operator, a field to apply the query to and a value to match
32 * against the query contents.
34 * <p>Valid operators include:
36 * <li> match - Field must contain the supplied value to produce a match. </li>
37 * <li> not-match - Field must NOT contain the supplied value to produce a match. </li>
39 * The following examples illustrate the structure of a few variants of the
43 * // Single Field Match Query:
45 * "match": {"field": "searchTags", "value": "abcd"}
48 * // Single Field Not-Match query:
50 * "not-match": {"field": "searchTags", "value": "efgh"}
55 * // Multi Field Match Query With A Single Value:
57 * "match": {"field": "entityType searchTags", "value": "pserver"}
60 * // Multi Field Match Query With Multiple Values:
62 * "match": {"field": "entityType searchTags", "value": "pserver tenant"}
66 public class TermQuery {
69 * The name of the field to apply the term query to.
74 * The value which the field must contain in order to have a match.
79 * For multi field queries only. Determines the rules for whether or not a document matches
80 * the query, as follows:
82 * <p>"and" - At least one occurrence of every supplied value must be present in any of the
85 * <p>"or" - At least one occurrence of any of the supplied values must be present in any of
86 * the supplied fields.
88 private String operator;
90 @JsonProperty("analyzer")
91 private String searchAnalyzer;
94 public String getField() {
98 public void setField(String field) {
102 public Object getValue() {
106 public void setValue(Object value) {
110 private boolean isNumericValue() {
111 return ((value instanceof Integer) || (value instanceof Double));
114 public String getOperator() {
118 public void setOperator(String operator) {
119 this.operator = operator;
122 public String getSearchAnalyzer() {
123 return searchAnalyzer;
126 public void setSearchAnalyzer(String searchAnalyzer) {
127 this.searchAnalyzer = searchAnalyzer;
131 * This method returns a string which represents this query in syntax
132 * that is understandable by ElasticSearch and is suitable for inclusion
133 * in an ElasticSearch query string.
135 * @return - ElasticSearch syntax string.
137 public String toElasticSearch() {
139 StringBuilder sb = new StringBuilder();
143 // Are we generating a multi field query?
144 if (isMultiFieldQuery()) {
146 // For multi field queries, we have to be careful about how we handle
147 // nested fields, so check to see if any of the specified fields are
149 if (field.contains(".")) {
151 // Build the equivalent of a multi match query across one or more nested fields.
152 toElasticSearchNestedMultiMatchQuery(sb);
156 // Build a real multi match query, since we don't need to worry about nested fields.
157 toElasticSearchMultiFieldQuery(sb);
161 // Single field query.
163 // Add the necessary wrapping if this is a query against a nested field.
164 if (fieldIsNested(field)) {
165 sb.append("{\"nested\": { \"path\": \"").append(pathForNestedField(field))
166 .append("\", \"query\": ");
170 toElasticSearchSingleFieldQuery(sb);
172 if (fieldIsNested(field)) {
179 return sb.toString();
184 * Determines whether or not the client has specified a term query with
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)
241 ? operator.toLowerCase() : "and").append("\"");
243 if (searchAnalyzer != null) {
244 sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
252 * Constructs the equivalent of an ElasticSearch multi match query across
253 * multiple nested fields.
255 * <p>Since ElasticSearch doesn't really let you do that, we have to be clever
256 * and construct an equivalent query using boolean operators to produce
259 * @param sb - The string builder to use to build the query.
261 public void toElasticSearchNestedMultiMatchQuery(StringBuilder sb) {
263 // Break out our whitespace delimited list of fields and values into a actual lists.
264 List<String> fields = Arrays.asList(field.split(" "));
265 List<String> values = Arrays.asList(((String) value).split(" ")); // GDF: revisit this cast.
267 sb.append("\"bool\": {");
269 if (operator != null) {
271 if (operator.toLowerCase().equals("and")) {
272 sb.append("\"must\": [");
273 } else if (operator.toLowerCase().equals("or")) {
274 sb.append("\"should\": [");
278 sb.append("\"must\": [");
281 AtomicBoolean firstField = new AtomicBoolean(true);
282 for (String f : fields) {
284 if (!firstField.compareAndSet(true, false)) {
290 // Is this a nested field?
291 if (fieldIsNested(f)) {
293 sb.append("\"nested\": {");
294 sb.append("\"path\": \"").append(pathForNestedField(f)).append("\", ");
295 sb.append("\"query\": ");
298 sb.append("{\"bool\": {");
299 sb.append("\"should\": [");
301 AtomicBoolean firstValue = new AtomicBoolean(true);
302 for (String v : values) {
303 if (!firstValue.compareAndSet(true, false)) {
306 sb.append("{\"match\": { \"");
307 sb.append(f).append("\": {\"query\": \"").append(v).append("\"");
309 if (searchAnalyzer != null) {
310 sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
318 if (fieldIsNested(f)) {
332 public String toString() {
333 return "field: " + field + ", value: " + value + " (" + value.getClass().getName() + ")";
336 public boolean fieldIsNested(String field) {
337 return field.contains(".");
340 public String pathForNestedField(String field) {
341 int index = field.lastIndexOf('.');
342 return field.substring(0, index);