2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
6 * Copyright © 2017 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 * ECOMP is a trademark and service mark of AT&T Intellectual Property.
23 package org.onap.aai.sa.searchdbabstraction.searchapi;
25 import com.fasterxml.jackson.annotation.JsonProperty;
26 import edu.emory.mathcs.backport.java.util.Arrays;
28 import java.util.List;
29 import java.util.concurrent.atomic.AtomicBoolean;
32 * This class represents a simple term query.
34 * <p>A term query takes an operator, a field to apply the query to and a value to match
35 * against the query contents.
37 * <p>Valid operators include:
39 * <li> match - Field must contain the supplied value to produce a match. </li>
40 * <li> not-match - Field must NOT contain the supplied value to produce a match. </li>
42 * The following examples illustrate the structure of a few variants of the
46 * // Single Field Match Query:
48 * "match": {"field": "searchTags", "value": "abcd"}
51 * // Single Field Not-Match query:
53 * "not-match": {"field": "searchTags", "value": "efgh"}
58 * // Multi Field Match Query With A Single Value:
60 * "match": {"field": "entityType searchTags", "value": "pserver"}
63 * // Multi Field Match Query With Multiple Values:
65 * "match": {"field": "entityType searchTags", "value": "pserver tenant"}
69 public class TermQuery {
72 * The name of the field to apply the term query to.
77 * The value which the field must contain in order to have a match.
82 * For multi field queries only. Determines the rules for whether or not a document matches
83 * the query, as follows:
85 * <p>"and" - At least one occurrence of every supplied value must be present in any of the
88 * <p>"or" - At least one occurrence of any of the supplied values must be present in any of
89 * 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
135 * that is understandable by ElasticSearch and is suitable for inclusion
136 * in an ElasticSearch query string.
138 * @return - ElasticSearch syntax string.
140 public String toElasticSearch() {
142 StringBuilder sb = new StringBuilder();
146 // Are we generating a multi field query?
147 if (isMultiFieldQuery()) {
149 // For multi field queries, we have to be careful about how we handle
150 // nested fields, so check to see if any of the specified fields are
152 if (field.contains(".")) {
154 // Build the equivalent of a multi match query across one or more nested fields.
155 toElasticSearchNestedMultiMatchQuery(sb);
159 // Build a real multi match query, since we don't need to worry about nested fields.
160 toElasticSearchMultiFieldQuery(sb);
164 // Single field query.
166 // Add the necessary wrapping if this is a query against a nested field.
167 if (fieldIsNested(field)) {
168 sb.append("{\"nested\": { \"path\": \"").append(pathForNestedField(field))
169 .append("\", \"query\": ");
173 toElasticSearchSingleFieldQuery(sb);
175 if (fieldIsNested(field)) {
182 return sb.toString();
187 * Determines whether or not the client has specified a term query with
190 * @return - true if the query is referencing multiple fields, false, otherwise.
192 private boolean isMultiFieldQuery() {
194 return (field.split(" ").length > 1);
199 * Constructs a single field term query in ElasticSearch syntax.
201 * @param sb - The string builder to assemble the query string with.
202 * @return - The single term query.
204 private void toElasticSearchSingleFieldQuery(StringBuilder sb) {
206 sb.append("\"term\": {\"").append(field).append("\" : ");
208 // For numeric values, don't enclose the value in quotes.
209 if (!isNumericValue()) {
210 sb.append("\"").append(value).append("\"");
220 * Constructs a multi field query in ElasticSearch syntax.
222 * @param sb - The string builder to assemble the query string with.
223 * @return - The multi field query.
225 private void toElasticSearchMultiFieldQuery(StringBuilder sb) {
227 sb.append("\"multi_match\": {");
229 sb.append("\"query\": \"").append(value).append("\", ");
230 sb.append("\"type\": \"cross_fields\",");
231 sb.append("\"fields\": [");
233 List<String> fields = Arrays.asList(field.split(" "));
234 AtomicBoolean firstField = new AtomicBoolean(true);
235 for (String f : fields) {
236 if (!firstField.compareAndSet(true, false)) {
239 sb.append("\"").append(f.trim()).append("\"");
243 sb.append("\"operator\": \"").append((operator != null)
244 ? operator.toLowerCase() : "and").append("\"");
246 if (searchAnalyzer != null) {
247 sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
255 * Constructs the equivalent of an ElasticSearch multi match query across
256 * multiple nested fields.
258 * <p>Since ElasticSearch doesn't really let you do that, we have to be clever
259 * and construct an equivalent query using boolean operators to produce
262 * @param sb - The string builder to use to build the query.
264 public void toElasticSearchNestedMultiMatchQuery(StringBuilder sb) {
266 // Break out our whitespace delimited list of fields and values into a actual lists.
267 List<String> fields = Arrays.asList(field.split(" "));
268 List<String> values = Arrays.asList(((String) value).split(" ")); // GDF: revisit this cast.
270 sb.append("\"bool\": {");
272 if (operator != null) {
274 if (operator.toLowerCase().equals("and")) {
275 sb.append("\"must\": [");
276 } else if (operator.toLowerCase().equals("or")) {
277 sb.append("\"should\": [");
281 sb.append("\"must\": [");
284 AtomicBoolean firstField = new AtomicBoolean(true);
285 for (String f : fields) {
287 if (!firstField.compareAndSet(true, false)) {
293 // Is this a nested field?
294 if (fieldIsNested(f)) {
296 sb.append("\"nested\": {");
297 sb.append("\"path\": \"").append(pathForNestedField(f)).append("\", ");
298 sb.append("\"query\": ");
301 sb.append("{\"bool\": {");
302 sb.append("\"should\": [");
304 AtomicBoolean firstValue = new AtomicBoolean(true);
305 for (String v : values) {
306 if (!firstValue.compareAndSet(true, false)) {
309 sb.append("{\"match\": { \"");
310 sb.append(f).append("\": {\"query\": \"").append(v).append("\"");
312 if (searchAnalyzer != null) {
313 sb.append(", \"analyzer\": \"").append(searchAnalyzer).append("\"");
321 if (fieldIsNested(f)) {
335 public String toString() {
336 return "field: " + field + ", value: " + value + " (" + value.getClass().getName() + ")";
339 public boolean fieldIsNested(String field) {
340 return field.contains(".");
343 public String pathForNestedField(String field) {
344 int index = field.lastIndexOf('.');
345 return field.substring(0, index);