77import co .elastic .clients .elasticsearch ._types .query_dsl .BoolQuery .Builder ;
88import co .elastic .clients .elasticsearch .core .SearchRequest ;
99import org .phoebus .applications .saveandrestore .model .Tag ;
10+ import org .phoebus .applications .saveandrestore .model .search .SearchQueryUtil ;
1011import org .springframework .beans .factory .annotation .Value ;
1112import org .springframework .http .HttpStatus ;
13+ import org .springframework .ldap .core .support .AbstractContextSource ;
1214import org .springframework .util .MultiValueMap ;
1315import org .springframework .web .server .ResponseStatusException ;
1416
2022import java .util .Map .Entry ;
2123import java .util .stream .Collectors ;
2224
25+ import org .slf4j .Logger ;
26+ import org .slf4j .LoggerFactory ;
27+
2328/**
2429 * A utility class for creating a search query for log entries based on time,
2530 * logbooks, tags, properties, description, etc.
@@ -45,6 +50,8 @@ public class SearchUtil {
4550 @ Value ("${elasticsearch.result.size.search.max:1000}" )
4651 private int maxSearchSize ;
4752
53+ private static final Logger LOG = LoggerFactory .getLogger (SearchUtil .class );
54+
4855 /**
4956 * @param searchParameters - the various search parameters
5057 * @return A {@link SearchRequest} based on the provided search parameters
@@ -53,6 +60,7 @@ public SearchRequest buildSearchRequest(MultiValueMap<String, String> searchPara
5360 Builder boolQueryBuilder = new Builder ();
5461 boolean fuzzySearch = false ;
5562 List <String > descriptionTerms = new ArrayList <>();
63+ List <String > descriptionPhraseTerms = new ArrayList <>();
5664 List <String > nodeNameTerms = new ArrayList <>();
5765 List <String > nodeNamePhraseTerms = new ArrayList <>();
5866 List <String > nodeTypeTerms = new ArrayList <>();
@@ -63,6 +71,9 @@ public SearchRequest buildSearchRequest(MultiValueMap<String, String> searchPara
6371 int searchResultSize = defaultSearchSize ;
6472 int from = 0 ;
6573
74+ LOG .info ("buildSearchRequest() called" );
75+ LOG .info (" searchParameters: " + searchParameters );
76+
6677 for (Entry <String , List <String >> parameter : searchParameters .entrySet ()) {
6778 switch (parameter .getKey ().strip ().toLowerCase ()) {
6879 case "uniqueid" :
@@ -72,29 +83,41 @@ public SearchRequest buildSearchRequest(MultiValueMap<String, String> searchPara
7283 }
7384 }
7485 break ;
86+
7587 // Search for node name. List of names cannot be split on space char as it is allowed in a node name.
7688 case "name" :
7789 for (String value : parameter .getValue ()) {
78- for (String pattern : value . split ( "[|,;]" )) {
90+ for (String pattern : getSearchTerms ( value )) {
7991 String term = pattern .trim ().toLowerCase ();
92+ // Quoted strings will be mapped to a phrase query
8093 if (term .startsWith ("\" " ) && term .endsWith ("\" " )){
8194 nodeNamePhraseTerms .add (term .substring (1 , term .length () - 1 ));
8295 }
8396 else {
84- nodeNameTerms .add (term );
97+ // add wildcards inorder to search for sub-strings
98+ nodeNameTerms .add ("*" + term + "*" );
8599 }
86100 }
87101 }
88102 break ;
103+
89104 // Search in description/comment
90- case "description" :
91105 case "desc" :
106+ case "description" :
92107 for (String value : parameter .getValue ()) {
93- for (String pattern : value .split ("[|,;]" )) {
94- descriptionTerms .add (pattern .trim ());
108+ for (String pattern : getSearchTerms (value )) {
109+ String term = pattern .trim ().toLowerCase ();
110+ // Quoted strings will be mapped to a phrase query
111+ if (term .startsWith ("\" " ) && term .endsWith ("\" " )) {
112+ descriptionPhraseTerms .add (term .substring (1 , term .length () - 1 ));
113+ } else {
114+ // add wildcards inorder to search for sub-strings
115+ descriptionTerms .add ("*" + term + "*" );
116+ }
95117 }
96118 }
97119 break ;
120+
98121 // Search for node type.
99122 case "type" :
100123 for (String value : parameter .getValue ()) {
@@ -212,71 +235,57 @@ public SearchRequest buildSearchRequest(MultiValueMap<String, String> searchPara
212235 }
213236 }
214237
215- // Add the description query
238+ // Add the description query. Multiple search terms will be AND:ed.
216239 if (!descriptionTerms .isEmpty ()) {
217- DisMaxQuery . Builder descQuery = new DisMaxQuery . Builder ();
218- List < Query > descQueries = new ArrayList <>() ;
219- if (fuzzySearch ) {
220- descriptionTerms . forEach ( searchTerm -> {
221- Query fuzzyQuery = FuzzyQuery .of (f -> f . field ("node.description " ).value ( searchTerm )) ._toQuery ();
222- NestedQuery nestedQuery =
223- NestedQuery .of (n1 -> n1 . path ("node" )
224- . query (fuzzyQuery ));
225- descQueries . add ( nestedQuery . _toQuery ());
226- } );
227- } else {
228- descriptionTerms . forEach ( searchTerm -> {
229- Query wildcardQuery =
230- WildcardQuery . of ( w -> w . field ( "node.description" ). value ( searchTerm )). _toQuery ();
231- NestedQuery nestedQuery =
232- NestedQuery . of ( n1 -> n1 . path ( "node" )
233- . query (wildcardQuery ));
234- descQueries . add ( nestedQuery . _toQuery ());
235- } );
240+ for ( String searchTerm : descriptionTerms ) {
241+ NestedQuery innerNestedQuery ;
242+ if (fuzzySearch ) {
243+ FuzzyQuery matchQuery = FuzzyQuery . of ( m -> m . field ( "node.description" ). value ( searchTerm ));
244+ innerNestedQuery = NestedQuery .of (n -> n . path ("node" ).query ( matchQuery ._toQuery ()) );
245+ } else {
246+ WildcardQuery matchQuery = WildcardQuery .of (m -> m . field ("node.description" ). value ( searchTerm ));
247+ innerNestedQuery = NestedQuery . of ( n -> n . path ( "node" ). query (matchQuery . _toQuery () ));
248+ }
249+ boolQueryBuilder . must ( innerNestedQuery . _toQuery () );
250+ }
251+ }
252+
253+ // Add phrase queries for the description key. Multiple search terms will be AND:ed.
254+ if (! descriptionPhraseTerms . isEmpty ()) {
255+ for ( String searchTerm : descriptionPhraseTerms ) {
256+ MatchPhraseQuery matchQuery = MatchPhraseQuery . of ( m -> m . field ( "node.description" ). query (searchTerm ));
257+ NestedQuery innerNestedQuery = NestedQuery . of ( n -> n . path ( "node" ). query ( matchQuery . _toQuery () ));
258+ boolQueryBuilder . must ( innerNestedQuery . _toQuery () );
236259 }
237- descQuery .queries (descQueries );
238- boolQueryBuilder .must (descQuery .build ()._toQuery ());
239260 }
240261
241262 // Add uniqueId query
242263 if (!uniqueIdTerms .isEmpty ()){
243264 boolQueryBuilder .must (IdsQuery .of (id -> id .values (uniqueIdTerms ))._toQuery ());
244265 }
245266
246- // Add the name query
267+ // Add the description query. Multiple search terms will be AND:ed.
247268 if (!nodeNameTerms .isEmpty ()) {
248- DisMaxQuery .Builder nodeNameQuery = new DisMaxQuery .Builder ();
249- List <Query > nodeNameQueries = new ArrayList <>();
250- if (fuzzySearch ) {
251- nodeNameTerms .forEach (searchTerm -> {
252- NestedQuery innerNestedQuery ;
269+ for (String searchTerm : nodeNameTerms ) {
270+ NestedQuery innerNestedQuery ;
271+ if (fuzzySearch ) {
253272 FuzzyQuery matchQuery = FuzzyQuery .of (m -> m .field ("node.name" ).value (searchTerm ));
254- innerNestedQuery = NestedQuery .of (n1 -> n1 .path ("node" ).query (matchQuery ._toQuery ()));
255- nodeNameQueries .add (innerNestedQuery ._toQuery ());
256- });
257- } else {
258- nodeNameTerms .forEach (searchTerm -> {
259- NestedQuery innerNestedQuery ;
273+ innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
274+ } else {
260275 WildcardQuery matchQuery = WildcardQuery .of (m -> m .field ("node.name" ).value (searchTerm ));
261- innerNestedQuery = NestedQuery .of (n1 -> n1 .path ("node" ).query (matchQuery ._toQuery ()));
262- nodeNameQueries . add ( innerNestedQuery . _toQuery ());
263- } );
276+ innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
277+ }
278+ boolQueryBuilder . must ( innerNestedQuery . _toQuery () );
264279 }
265- nodeNameQuery .queries (nodeNameQueries );
266- boolQueryBuilder .must (nodeNameQuery .build ()._toQuery ());
267280 }
268281
269- if (!nodeNamePhraseTerms .isEmpty ()){
270- DisMaxQuery .Builder nodeNamePhraseQueryBuilder = new DisMaxQuery .Builder ();
271- List <NestedQuery > nestedQueries = new ArrayList <>();
272- nodeNamePhraseTerms .forEach (phraseSearchTerm -> {
273- NestedQuery innerNestedQuery ;
274- MatchPhraseQuery matchPhraseQuery = MatchPhraseQuery .of (m -> m .field ("node.name" ).query (phraseSearchTerm ));
275- innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchPhraseQuery ._toQuery ()));
276- nestedQueries .add (innerNestedQuery );
277- });
278- nodeNamePhraseQueryBuilder .queries (nestedQueries .stream ().map (QueryVariant ::_toQuery ).collect (Collectors .toList ()));
279- boolQueryBuilder .must (nodeNamePhraseQueryBuilder .build ()._toQuery ());
282+ // Add phrase queries for the nodeName key. Multiple search terms will be AND:ed.
283+ if (!nodeNamePhraseTerms .isEmpty ()) {
284+ for (String searchTerm : nodeNamePhraseTerms ) {
285+ MatchPhraseQuery matchQuery = MatchPhraseQuery .of (m -> m .field ("node.name" ).query (searchTerm ));
286+ NestedQuery innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
287+ boolQueryBuilder .must (innerNestedQuery ._toQuery ());
288+ }
280289 }
281290
282291 // Add node type query. Fuzzy search not needed as node types are well-defined and limited in number.
@@ -337,4 +346,43 @@ public SearchRequest buildSearchRequestForPvs(List<String> pvNames) {
337346 .size (Math .min (searchResultSize , maxSearchSize ))
338347 .from (0 ));
339348 }
349+
350+ /**
351+ * Parses a search query terms string into a string array. In particular,
352+ * quoted search terms must be maintained even if they contain the
353+ * separator chars used to tokenize the terms.
354+ *
355+ * @param searchQueryTerms String as specified by client
356+ * @return A {@link List} of search terms, some of which may be
357+ * quoted. Is void of any zero-length strings.
358+ */
359+ public List <String > getSearchTerms (String searchQueryTerms ) {
360+ // Count double quote chars. Odd number of quote chars
361+ // is not supported -> throw exception
362+ long quoteCount = searchQueryTerms .chars ().filter (c -> c == '\"' ).count ();
363+ if (quoteCount == 0 ) {
364+ return Arrays .stream (searchQueryTerms .split ("[\\ |,;\\ s+]" )).filter (t -> t .length () > 0 ).collect (Collectors .toList ());
365+ }
366+ if (quoteCount % 2 == 1 ) {
367+ throw new IllegalArgumentException ("Unbalanced quotes in search query" );
368+ }
369+ // If we come this far then at least one quoted term is
370+ // contained in user input
371+ List <String > terms = new ArrayList <>();
372+ int nextStartIndex = searchQueryTerms .indexOf ('\"' );
373+ while (nextStartIndex >= 0 ) {
374+ int endIndex = searchQueryTerms .indexOf ('\"' , nextStartIndex + 1 );
375+ String quotedTerm = searchQueryTerms .substring (nextStartIndex , endIndex + 1 );
376+ terms .add (quotedTerm );
377+ // Remove the quoted term from user input
378+ searchQueryTerms = searchQueryTerms .replace (quotedTerm , "" );
379+ // Check next occurrence
380+ nextStartIndex = searchQueryTerms .indexOf ('\"' );
381+ }
382+ // Add remaining terms...
383+ List <String > remaining = Arrays .asList (searchQueryTerms .split ("[\\ |,;\\ s+]" ));
384+ //...but remove empty strings, which are "leftovers" when quoted terms are removed
385+ terms .addAll (remaining .stream ().filter (t -> t .length () > 0 ).collect (Collectors .toList ()));
386+ return terms ;
387+ }
340388}
0 commit comments