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,7 +71,11 @@ 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 ()) {
78+ LOG .info (" key: " + parameter .getKey ().strip ().toLowerCase ());
6779 switch (parameter .getKey ().strip ().toLowerCase ()) {
6880 case "uniqueid" :
6981 for (String value : parameter .getValue ()) {
@@ -75,26 +87,42 @@ public SearchRequest buildSearchRequest(MultiValueMap<String, String> searchPara
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+ LOG .info (" value: [" + value + "]" );
91+ for (String pattern : getSearchTerms (value )) {
92+ // for (String pattern : value.split("[|,;]")) {
7993 String term = pattern .trim ().toLowerCase ();
94+ LOG .info (" term: [" + term + "]" );
95+ // Quoted strings will be mapped to a phrase query
8096 if (term .startsWith ("\" " ) && term .endsWith ("\" " )){
8197 nodeNamePhraseTerms .add (term .substring (1 , term .length () - 1 ));
8298 }
8399 else {
84- nodeNameTerms .add (term );
100+ // add wildcards inorder to search for sub-strings
101+ nodeNameTerms .add ("*" + term + "*" );
85102 }
86103 }
87104 }
88105 break ;
106+
89107 // Search in description/comment
90- case "description" :
91108 case "desc" :
109+ case "description" :
92110 for (String value : parameter .getValue ()) {
93- for (String pattern : value .split ("[|,;]" )) {
94- descriptionTerms .add (pattern .trim ());
111+ LOG .info (" value: [" + value + "]" );
112+ for (String pattern : getSearchTerms (value )) {
113+ String term = pattern .trim ().toLowerCase ();
114+ LOG .info (" term: [" + term + "]" );
115+ // Quoted strings will be mapped to a phrase query
116+ if (term .startsWith ("\" " ) && term .endsWith ("\" " )) {
117+ descriptionPhraseTerms .add (term .substring (1 , term .length () - 1 ));
118+ } else {
119+ // add wildcards inorder to search for sub-strings
120+ descriptionTerms .add ("*" + term + "*" );
121+ }
95122 }
96123 }
97124 break ;
125+
98126 // Search for node type.
99127 case "type" :
100128 for (String value : parameter .getValue ()) {
@@ -212,71 +240,67 @@ public SearchRequest buildSearchRequest(MultiValueMap<String, String> searchPara
212240 }
213241 }
214242
215- // Add the description query
243+ LOG .info (" descriptionTerms: " + descriptionTerms );
244+ LOG .info (" descriptionTerms.isEmpty() : " + descriptionTerms .isEmpty ());
245+ // Add the description query. Multiple search terms will be AND:ed.
216246 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- });
247+ LOG .info (" fuzzySearch: " + fuzzySearch );
248+ for (String searchTerm : descriptionTerms ) {
249+ NestedQuery innerNestedQuery ;
250+ if (fuzzySearch ) {
251+ FuzzyQuery matchQuery = FuzzyQuery .of (m -> m .field ("node.description" ).value (searchTerm ));
252+ innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
253+ } else {
254+ WildcardQuery matchQuery = WildcardQuery .of (m -> m .field ("node.description" ).value (searchTerm ));
255+ innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
256+ }
257+ boolQueryBuilder .must (innerNestedQuery ._toQuery ());
258+ }
259+ }
260+
261+ LOG .info (" descriptionPhraseTerms: " + descriptionPhraseTerms );
262+ LOG .info (" descriptionPhraseTerms.isEmpty() : " + descriptionPhraseTerms .isEmpty ());
263+ // Add phrase queries for the description key. Multiple search terms will be AND:ed.
264+ if (!descriptionPhraseTerms .isEmpty ()) {
265+ for (String searchTerm : descriptionPhraseTerms ) {
266+ MatchPhraseQuery matchQuery = MatchPhraseQuery .of (m -> m .field ("node.description" ).query (searchTerm ));
267+ NestedQuery innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
268+ boolQueryBuilder .must (innerNestedQuery ._toQuery ());
236269 }
237- descQuery .queries (descQueries );
238- boolQueryBuilder .must (descQuery .build ()._toQuery ());
239270 }
240271
241272 // Add uniqueId query
242273 if (!uniqueIdTerms .isEmpty ()){
243274 boolQueryBuilder .must (IdsQuery .of (id -> id .values (uniqueIdTerms ))._toQuery ());
244275 }
245276
246- // Add the name query
277+ LOG .info (" nodeNameTerms: " + nodeNameTerms );
278+ LOG .info (" nodeNameTerms.isEmpty() : " + nodeNameTerms .isEmpty ());
279+ // Add the description query. Multiple search terms will be AND:ed.
247280 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 ;
281+ LOG .info (" fuzzySearch: " + fuzzySearch );
282+ for (String searchTerm : nodeNameTerms ) {
283+ NestedQuery innerNestedQuery ;
284+ if (fuzzySearch ) {
253285 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 ;
286+ innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
287+ } else {
260288 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- } );
289+ innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
290+ }
291+ boolQueryBuilder . must ( innerNestedQuery . _toQuery () );
264292 }
265- nodeNameQuery .queries (nodeNameQueries );
266- boolQueryBuilder .must (nodeNameQuery .build ()._toQuery ());
267293 }
268294
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 ());
295+ LOG .info (" nodeNamePhraseTerms: " + nodeNamePhraseTerms );
296+ LOG .info (" nodeNamePhraseTerms.isEmpty() : " + nodeNamePhraseTerms .isEmpty ());
297+ // Add phrase queries for the nodeName key. Multiple search terms will be AND:ed.
298+ if (!nodeNamePhraseTerms .isEmpty ()) {
299+ for (String searchTerm : nodeNamePhraseTerms ) {
300+ MatchPhraseQuery matchQuery = MatchPhraseQuery .of (m -> m .field ("node.name" ).query (searchTerm ));
301+ NestedQuery innerNestedQuery = NestedQuery .of (n -> n .path ("node" ).query (matchQuery ._toQuery ()));
302+ boolQueryBuilder .must (innerNestedQuery ._toQuery ());
303+ }
280304 }
281305
282306 // Add node type query. Fuzzy search not needed as node types are well-defined and limited in number.
@@ -337,4 +361,43 @@ public SearchRequest buildSearchRequestForPvs(List<String> pvNames) {
337361 .size (Math .min (searchResultSize , maxSearchSize ))
338362 .from (0 ));
339363 }
364+
365+ /**
366+ * Parses a search query terms string into a string array. In particular,
367+ * quoted search terms must be maintained even if they contain the
368+ * separator chars used to tokenize the terms.
369+ *
370+ * @param searchQueryTerms String as specified by client
371+ * @return A {@link List} of search terms, some of which may be
372+ * quoted. Is void of any zero-length strings.
373+ */
374+ public List <String > getSearchTerms (String searchQueryTerms ) {
375+ // Count double quote chars. Odd number of quote chars
376+ // is not supported -> throw exception
377+ long quoteCount = searchQueryTerms .chars ().filter (c -> c == '\"' ).count ();
378+ if (quoteCount == 0 ) {
379+ return Arrays .stream (searchQueryTerms .split ("[\\ |,;\\ s+]" )).filter (t -> t .length () > 0 ).collect (Collectors .toList ());
380+ }
381+ if (quoteCount % 2 == 1 ) {
382+ throw new IllegalArgumentException ("Unbalanced quotes in search query" );
383+ }
384+ // If we come this far then at least one quoted term is
385+ // contained in user input
386+ List <String > terms = new ArrayList <>();
387+ int nextStartIndex = searchQueryTerms .indexOf ('\"' );
388+ while (nextStartIndex >= 0 ) {
389+ int endIndex = searchQueryTerms .indexOf ('\"' , nextStartIndex + 1 );
390+ String quotedTerm = searchQueryTerms .substring (nextStartIndex , endIndex + 1 );
391+ terms .add (quotedTerm );
392+ // Remove the quoted term from user input
393+ searchQueryTerms = searchQueryTerms .replace (quotedTerm , "" );
394+ // Check next occurrence
395+ nextStartIndex = searchQueryTerms .indexOf ('\"' );
396+ }
397+ // Add remaining terms...
398+ List <String > remaining = Arrays .asList (searchQueryTerms .split ("[\\ |,;\\ s+]" ));
399+ //...but remove empty strings, which are "leftovers" when quoted terms are removed
400+ terms .addAll (remaining .stream ().filter (t -> t .length () > 0 ).collect (Collectors .toList ()));
401+ return terms ;
402+ }
340403}
0 commit comments