Skip to content

Commit e7e33a6

Browse files
for description searches: made case insensitive, multiple terms function
as an AND, added phrase queries, added partial word searching
1 parent 20dd0cc commit e7e33a6

File tree

4 files changed

+128
-57
lines changed

4 files changed

+128
-57
lines changed

app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,13 +493,13 @@ private void updateParametersAndSearch() {
493493
*/
494494
private String buildQueryString() {
495495
Map<String, String> map = new HashMap<>();
496-
if (nodeNameProperty.get() != null && !nodeNameProperty.get().isEmpty()) {
496+
if (nodeNameProperty.get() != null && !nodeNameProperty.get().trim().isEmpty()) {
497497
map.put(Keys.NAME.getName(), nodeNameProperty.get());
498498
}
499499
if (userNameProperty.get() != null && !userNameProperty.get().isEmpty()) {
500500
map.put(Keys.USER.getName(), userNameProperty.get());
501501
}
502-
if (descProperty.get() != null && !descProperty.get().isEmpty()) {
502+
if (descProperty.get() != null && !descProperty.get().trim().isEmpty()) {
503503
map.put(Keys.DESC.getName(), descProperty.get());
504504
}
505505
if (tagsProperty.get() != null && !tagsProperty.get().isEmpty()) {

app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchResultTableViewController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,10 @@ public void search() {
328328

329329
public void search(final String query) {
330330
queryString = query;
331+
LOGGER.log(Level.INFO, "search() query: = " + queryString);
331332
Map<String, String> searchParams =
332333
SearchQueryUtil.parseHumanReadableQueryString(queryString);
334+
LOGGER.log(Level.INFO, "search() searchParams: = " + searchParams);
333335

334336
searchParams.put(SearchQueryUtil.Keys.FROM.getName(), Integer.toString(pagination.getCurrentPageIndex() * pageSizeProperty.get()));
335337
searchParams.put(SearchQueryUtil.Keys.SIZE.getName(), Integer.toString(pageSizeProperty.get()));

app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/SearchQueryUtil.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.Map;
2727
import java.util.function.Function;
2828
import java.util.stream.Collectors;
29+
import java.util.logging.Logger;
30+
import java.util.logging.Level;
2931

3032
public class SearchQueryUtil {
3133

@@ -77,6 +79,8 @@ public static Keys findKey(String keyName) {
7779
}
7880
}
7981

82+
private static final Logger LOGGER = Logger.getLogger(SearchQueryUtil.class.getName());
83+
8084
/**
8185
* This method parses a logbook query string and returns a map of search keys and their associated search patterns as
8286
* values.
@@ -120,6 +124,8 @@ public static String toQueryString(Map<String, String> queryParams){
120124
* @return A formatted string with trimmed values, e.g. "a,b" rather than " a , b".
121125
*/
122126
private static String formatSearchTerm(String searchTerm){
127+
LOGGER.log(Level.INFO, "formatSearchTerm() input: [" + searchTerm + "]");
128+
123129
if(searchTerm == null || searchTerm.isEmpty()){
124130
return "";
125131
}

services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/search/SearchUtil.java

Lines changed: 118 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery.Builder;
88
import co.elastic.clients.elasticsearch.core.SearchRequest;
99
import org.phoebus.applications.saveandrestore.model.Tag;
10+
import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil;
1011
import org.springframework.beans.factory.annotation.Value;
1112
import org.springframework.http.HttpStatus;
13+
import org.springframework.ldap.core.support.AbstractContextSource;
1214
import org.springframework.util.MultiValueMap;
1315
import org.springframework.web.server.ResponseStatusException;
1416

@@ -20,6 +22,9 @@
2022
import java.util.Map.Entry;
2123
import 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

Comments
 (0)