2 * ============LICENSE_START===================================================
3 * SPARKY (AAI UI service)
4 * ============================================================================
5 * Copyright © 2017 AT&T Intellectual Property.
6 * Copyright © 2017 Amdocs
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 at
13 * http://www.apache.org/licenses/LICENSE-2.0
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=====================================================
22 * ECOMP and OpenECOMP are trademarks
23 * and service marks of AT&T Intellectual Property.
25 package org.onap.aai.sparky.viewandinspect.search;
27 import java.io.IOException;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Iterator;
32 import java.util.List;
35 import org.onap.aai.cl.api.Logger;
36 import org.onap.aai.cl.eelf.LoggerFactory;
37 import org.onap.aai.restclient.client.OperationResult;
38 import org.onap.aai.sparky.common.search.CommonSearchSuggestion;
39 import org.onap.aai.sparky.config.oxm.OxmEntityDescriptor;
40 import org.onap.aai.sparky.config.oxm.OxmEntityLookup;
41 import org.onap.aai.sparky.logging.AaiUiMsgs;
42 import org.onap.aai.sparky.search.SearchServiceAdapter;
43 import org.onap.aai.sparky.search.api.SearchProvider;
44 import org.onap.aai.sparky.search.config.SuggestionConfig;
45 import org.onap.aai.sparky.search.entity.QuerySearchEntity;
46 import org.onap.aai.sparky.search.entity.SearchSuggestion;
47 import org.onap.aai.sparky.util.NodeUtils;
48 import org.onap.aai.sparky.viewandinspect.config.SparkyConstants;
50 import com.fasterxml.jackson.databind.JsonNode;
51 import com.fasterxml.jackson.databind.ObjectMapper;
52 import com.fasterxml.jackson.databind.node.ArrayNode;
54 public class ViewInspectSearchProvider implements SearchProvider {
56 private static final Logger LOG =
57 LoggerFactory.getInstance().getLogger(ViewInspectSearchProvider.class);
59 private SearchServiceAdapter searchServiceAdapter = null;
60 private SuggestionConfig suggestionConfig;
61 private String additionalSearchSuggestionText;
63 private static final String KEY_SEARCH_RESULT = "searchResult";
64 private static final String KEY_HITS = "hits";
65 private static final String KEY_DOCUMENT = "document";
66 private static final String KEY_CONTENT = "content";
68 private static final String KEY_SEARCH_TAG_IDS = "searchTagIDs";
69 private static final String KEY_SEARCH_TAGS = "searchTags";
70 private static final String KEY_LINK = "link";
71 private static final String KEY_ENTITY_TYPE = "entityType";
73 private final String viewInspectIndexName;
74 private final String viewInspectSuggestionRoute;
75 private OxmEntityLookup oxmEntityLookup;
77 public ViewInspectSearchProvider(SearchServiceAdapter searchServiceAdapter,
78 SuggestionConfig suggestionConfig, String viewInspectIndexName,
79 String viewInspectSuggestionRoute, OxmEntityLookup oxmEntityLookup) throws Exception {
81 this.searchServiceAdapter = searchServiceAdapter;
82 this.oxmEntityLookup = oxmEntityLookup;
83 this.suggestionConfig = suggestionConfig;
84 additionalSearchSuggestionText = null;
85 this.viewInspectIndexName = viewInspectIndexName;
86 this.viewInspectSuggestionRoute = viewInspectSuggestionRoute;
91 public List<SearchSuggestion> search(QuerySearchEntity queryRequest) {
93 List<SearchSuggestion> suggestionEntityList = new ArrayList<SearchSuggestion>();
96 * Based on the configured stop words, we need to strip any matched stop-words ( case
97 * insensitively ) from the query string, before hitting elastic to prevent the words from being
98 * used against the elastic view-and-inspect index. Another alternative to this approach would
99 * be to define stop words on the elastic search index configuration for the
100 * entity-search-index, but but that may be more complicated / more risky than just a simple bug
101 * fix, but it's something we should think about for the future.
105 final String queryStringWithoutStopWords =
106 stripStopWordsFromQuery(queryRequest.getQueryStr());
108 final String fullUrlStr = searchServiceAdapter.buildSearchServiceQueryUrl(viewInspectIndexName);
110 String postBody = String.format(VIUI_SEARCH_TEMPLATE, Integer.parseInt(queryRequest.getMaxResults()),
111 queryStringWithoutStopWords);
113 OperationResult opResult = searchServiceAdapter.doPost(fullUrlStr, postBody, "application/json");
114 if (opResult.getResultCode() == 200) {
115 suggestionEntityList =
116 generateSuggestionsForSearchResponse(opResult.getResult(), queryRequest.getQueryStr());
118 } catch (Exception exc) {
119 LOG.error(AaiUiMsgs.SEARCH_SERVLET_ERROR,
120 "View and inspect query failed with error = " + exc.getMessage());
122 return suggestionEntityList;
127 public String getAdditionalSearchSuggestionText() {
128 return additionalSearchSuggestionText;
131 public void setAdditionalSearchSuggestionText(String additionalSearchSuggestionText) {
132 this.additionalSearchSuggestionText = additionalSearchSuggestionText;
139 * Builds the search response.
141 * @param operationResult The Elasticsearch query result
142 * @param queryStr The string the user typed into the search bar
143 * @return A list of search suggestions and corresponding UI filter values
145 private List<SearchSuggestion> generateSuggestionsForSearchResponse(String operationResult,
149 if (operationResult == null || operationResult.length() == 0) {
153 ObjectMapper mapper = new ObjectMapper();
154 JsonNode rootNode = null;
155 List<SearchSuggestion> suggestionEntityList = new ArrayList<SearchSuggestion>();
157 rootNode = mapper.readTree(operationResult);
159 JsonNode hitsNode = rootNode.get(KEY_SEARCH_RESULT);
163 // Check if there are hits that are coming back
164 if (hitsNode.has(KEY_HITS)) {
165 ArrayNode hitsArray = (ArrayNode) hitsNode.get(KEY_HITS);
168 * next we iterate over the values in the hit array elements
171 Iterator<JsonNode> nodeIterator = hitsArray.elements();
172 JsonNode entityNode = null;
173 CommonSearchSuggestion suggestionEntity = null;
174 JsonNode sourceNode = null;
175 while (nodeIterator.hasNext()) {
176 entityNode = nodeIterator.next();
177 sourceNode = entityNode.get(KEY_DOCUMENT).get(KEY_CONTENT);
179 // do the point transformation as we build the response?
180 suggestionEntity = new CommonSearchSuggestion();
181 suggestionEntity.setRoute(viewInspectSuggestionRoute);
184 * This is where we probably want to annotate the search tags because we also have access
188 String searchTagIds = getValueFromNode(sourceNode, KEY_SEARCH_TAG_IDS);
189 String searchTags = getValueFromNode(sourceNode, KEY_SEARCH_TAGS);
190 String entityType = getValueFromNode(sourceNode, KEY_ENTITY_TYPE);
191 String link = getValueFromNode(sourceNode, KEY_LINK);
194 suggestionEntity.setHashId(NodeUtils.generateUniqueShaDigest(link));
199 .setText(annotateSearchTags(searchTags, searchTagIds, entityType, queryStr));
200 } catch (Exception exc) {
201 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(),
202 exc.getLocalizedMessage());
203 // at least send back the un-annotated search tags
204 suggestionEntity.setText(searchTags);
207 if ( getAdditionalSearchSuggestionText() != null ) {
208 String suggestionText = suggestionEntity.getText() ;
209 suggestionText += SparkyConstants.SUGGESTION_TEXT_SEPARATOR
210 + getAdditionalSearchSuggestionText();
211 suggestionEntity.setText(suggestionText);
214 if (searchTags != null) {
215 suggestionEntityList.add(suggestionEntity);
220 } catch (IOException exc) {
221 LOG.warn(AaiUiMsgs.SEARCH_RESPONSE_BUILDING_EXCEPTION, exc.getLocalizedMessage());
223 return suggestionEntityList;
229 * The current format of an UI-dropdown-item is like: "search-terms entityType att1=attr1_val".
230 * Example, for pserver: search-terms pserver hostname=djmAG-72060,
231 * pserver-name2=example-pserver-name2-val-17254, pserver-id=example-pserver-id-val-17254,
232 * ipv4-oam-address=example-ipv4-oam-address-val-17254 SearchController.js parses the above
233 * format. So if you are modifying the parsing below, please update SearchController.js as well.
235 * @param searchTags the search tags
236 * @param searchTagIds the search tag ids
237 * @param entityType the entity type
238 * @param queryStr the query str
242 private String annotateSearchTags(String searchTags, String searchTagIds, String entityType,
245 if (searchTags == null || searchTagIds == null) {
246 String valueOfSearchTags = String.valueOf(searchTags);
247 String valueOfSearchTagIds = String.valueOf(searchTagIds);
249 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
250 "Search tags = " + valueOfSearchTags + " and Seach tag IDs = " + valueOfSearchTagIds);
254 if (entityType == null) {
255 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(), "EntityType is null");
259 if (queryStr == null) {
260 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, searchTags.toString(),
261 "Query string is null");
266 * The ElasticSearch analyzer has already applied the lowercase filter, so we don't have to
269 String[] searchTagsArray = searchTags.split(";");
270 String[] searchTagIdsArray = searchTagIds.split(";");
272 // specifically apply lower case to the the query terms to make matching
274 String[] queryTerms = queryStr.toLowerCase().split(" ");
276 OxmEntityDescriptor desc = oxmEntityLookup.getEntityDescriptors().get(entityType);
279 LOG.error(AaiUiMsgs.ENTITY_NOT_FOUND_IN_OXM, entityType.toString());
283 String primaryKeyName = NodeUtils.concatArray(desc.getPrimaryKeyAttributeNames(), "/");
284 String primaryKeyValue = null;
287 * For each used attribute, get the fieldName for the attribute index and transform the search
288 * tag into t1,t2,t3 => h1=t1, h2=t2, h3=t3;
290 StringBuilder searchTagsBuilder = new StringBuilder(128);
291 searchTagsBuilder.append(entityType);
293 String primaryKeyConjunctionValue = null;
294 boolean queryTermsMatchedSearchTags = false;
296 if (searchTagsArray.length == searchTagIdsArray.length) {
297 for (int i = 0; i < searchTagsArray.length; i++) {
298 String searchTagAttributeId = searchTagIdsArray[i];
299 String searchTagAttributeValue = searchTagsArray[i];
301 // Find the concat conjunction
302 Map<String, String> pairConjunctionList = suggestionConfig.getPairingList();
304 String suggConjunction = null;
305 if (pairConjunctionList.get(searchTagAttributeId) != null) {
306 suggConjunction = pairConjunctionList.get(searchTagAttributeId);
308 suggConjunction = suggestionConfig.getDefaultPairingValue();
311 if (primaryKeyName.equals(searchTagAttributeId)) {
312 primaryKeyValue = searchTagAttributeValue;
313 primaryKeyConjunctionValue = suggConjunction;
316 if (queryTermsMatchSearchTag(queryTerms, searchTagAttributeValue)) {
317 searchTagsBuilder.append(" " + suggConjunction + " " + searchTagAttributeValue);
318 queryTermsMatchedSearchTags = true;
322 String errorMessage =
323 "Search tags length did not match search tag ID length for entity type " + entityType;
324 LOG.error(AaiUiMsgs.ENTITY_SYNC_SEARCH_TAG_ANNOTATION_FAILED, errorMessage);
330 * if none of the user query terms matched the index entity search tags then we should still tag
331 * the matched entity with a conjunction set to at least it's entity primary key value to
332 * discriminate between the entities of the same type in the search results displayed in the UI
336 if (!queryTermsMatchedSearchTags) {
338 if (primaryKeyValue != null && primaryKeyConjunctionValue != null) {
339 searchTagsBuilder.append(" " + primaryKeyConjunctionValue + " " + primaryKeyValue);
341 LOG.error(AaiUiMsgs.SEARCH_TAG_ANNOTATION_ERROR, "See error",
342 "Could not annotate user query terms " + queryStr
343 + " from available entity search tags = " + searchTags);
349 return searchTagsBuilder.toString();
354 * Query terms match search tag.
356 * @param queryTerms the query terms
357 * @param searchTag the search tag
358 * @return true, if successful @return.
360 private boolean queryTermsMatchSearchTag(String[] queryTerms, String searchTag) {
362 if (queryTerms == null || queryTerms.length == 0 || searchTag == null) {
366 for (String queryTerm : queryTerms) {
367 if (searchTag.toLowerCase().contains(queryTerm.toLowerCase())) {
377 * Gets the value from node.
379 * @param node the node
380 * @param fieldName the field name
381 * @return the value from node
383 private String getValueFromNode(JsonNode node, String fieldName) {
385 if (node == null || fieldName == null) {
389 JsonNode valueNode = node.get(fieldName);
391 if (valueNode != null) {
392 return valueNode.asText();
399 private static final String VIUI_SEARCH_TEMPLATE =
400 "{ " + "\"results-start\": 0," + "\"results-size\": %d," + "\"queries\": [{" + "\"must\": {"
401 + "\"match\": {" + "\"field\": \"entityType searchTags crossEntityReferenceValues\","
402 + "\"value\": \"%s\"," + "\"operator\": \"and\", "
403 + "\"analyzer\": \"whitespace_analyzer\"" + "}" + "}" + "}]" + "}";
405 //private SuggestionConfig suggestionConfig = null;
408 * @param queryStr - space separate query search terms
409 * @return - query string with stop-words removed
411 private String stripStopWordsFromQuery(String queryStr) {
413 if (queryStr == null) {
417 Collection<String> stopWords = suggestionConfig.getStopWords();
418 ArrayList<String> queryTerms =
419 new ArrayList<String>(Arrays.asList(queryStr.toLowerCase().split(" ")));
421 queryTerms.removeAll(stopWords);
423 return String.join(" ", queryTerms);