From 45738ddbb87c794c2061d4142e37a162261aabe4 Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Sun, 15 Nov 2020 00:58:52 +0100 Subject: [PATCH] Simple generic search engine --- .../pandacube/util/search/SearchEngine.java | 178 ++++++++++++++++++ .../pandacube/util/search/SearchResult.java | 11 ++ 2 files changed, 189 insertions(+) create mode 100644 src/main/java/fr/pandacube/util/search/SearchEngine.java create mode 100644 src/main/java/fr/pandacube/util/search/SearchResult.java diff --git a/src/main/java/fr/pandacube/util/search/SearchEngine.java b/src/main/java/fr/pandacube/util/search/SearchEngine.java new file mode 100644 index 0000000..287d9fc --- /dev/null +++ b/src/main/java/fr/pandacube/util/search/SearchEngine.java @@ -0,0 +1,178 @@ +package fr.pandacube.util.search; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import fr.pandacube.util.Log; + +/** + * Utility class to manage searching among a set of + * SearchResult instances, using case insensitive + * keywords. + */ +public class SearchEngine { + + Map> searchKeywordsResultMap = new HashMap<>(); + Map> resultsSearchKeywordsMap = new HashMap<>(); + + Map> suggestionsKeywordsResultMap = new HashMap<>(); + Map> resultsSuggestionsKeywordsMap = new HashMap<>(); + + Set resultSet = new HashSet<>(); + + private Cache, List> suggestionsCache; + + public SearchEngine(int suggestionsCacheSize) { + suggestionsCache = CacheBuilder.newBuilder() + .maximumSize(suggestionsCacheSize) + .build(); + } + + public synchronized void addResult(R result) { + if (result == null) + throw new IllegalArgumentException("Provided result cannot be null."); + if (resultSet.contains(result)) + return; + + Set searchKw; + try { + searchKw = result.getSearchKeywords(); + Preconditions.checkNotNull(searchKw, "SearchResult instance must provide a non null set of search keywords"); + searchKw = searchKw.stream() + .filter(e -> e != null) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } catch (Exception e) { + Log.severe(e); + return; + } + + resultSet.add(result); + + for (String skw : searchKw) { + searchKeywordsResultMap.computeIfAbsent(skw, s -> new HashSet<>()).add(result); + } + + resultsSearchKeywordsMap.put(result, searchKw); + + Set suggestsKw; + try { + suggestsKw = result.getSuggestionKeywords(); + Preconditions.checkNotNull(suggestsKw, "SearchResult instance must provide a non null set of suggestions keywords"); + suggestsKw.removeIf(e -> e == null); + } catch (Exception e) { + Log.severe(e); + return; + } + + resultsSuggestionsKeywordsMap.put(result, new HashSet<>(suggestsKw)); + + for (String skw : suggestsKw) { + suggestionsKeywordsResultMap.computeIfAbsent(skw, s -> new HashSet<>()).add(result); + } + + suggestionsCache.invalidateAll(); + } + + public synchronized void removeResult(R result) { + if (result == null || !resultSet.contains(result)) + return; + + resultSet.remove(result); + + Set searchKw = resultsSearchKeywordsMap.remove(result); + for (String skw : searchKw) { + Set set = searchKeywordsResultMap.get(skw); + set.remove(result); + if (set.isEmpty()) + searchKeywordsResultMap.remove(skw); + } + + Set suggestsKw = resultsSearchKeywordsMap.remove(result); + for (String skw : suggestsKw) { + Set set = suggestionsKeywordsResultMap.get(skw); + set.remove(result); + if (set.isEmpty()) + suggestionsKeywordsResultMap.remove(skw); + } + + resultsSuggestionsKeywordsMap.remove(result); + + suggestionsCache.invalidateAll(); + } + + public synchronized Set search(Set searchTerms) { + if (searchTerms == null) + searchTerms = new HashSet(); + + Set retainedResults = new HashSet<>(resultSet); + for (String term : searchTerms) { + retainedResults.retainAll(search(term)); + } + + return retainedResults; + } + + public synchronized Set search(String searchTerm) { + if (searchTerm == null || searchTerm.isEmpty()) { + return new HashSet<>(resultSet); + } + searchTerm = searchTerm.toLowerCase(); + Set retainedResults = new HashSet<>(); + for (String skw : searchKeywordsResultMap.keySet()) { + if (skw.contains(searchTerm)) { + retainedResults.addAll(new ArrayList<>(searchKeywordsResultMap.get(skw))); + } + } + + return retainedResults; + } + + public synchronized List suggestKeywords(List prevSearchTerms) { + if (prevSearchTerms == null || prevSearchTerms.isEmpty()) { + return new ArrayList<>(suggestionsKeywordsResultMap.keySet()); + } + Set lowerCaseSearchTerm = prevSearchTerms.stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + + try { + return suggestionsCache.get(lowerCaseSearchTerm, (Callable>) () -> { + Set prevResults = search(lowerCaseSearchTerm); + + Set suggestions = new HashSet<>(); + for (R prevRes : prevResults) { + suggestions.addAll(new ArrayList<>(resultsSuggestionsKeywordsMap.get(prevRes))); + } + + suggestions.removeIf(s -> { + for (String st : lowerCaseSearchTerm) + if (s.contains(st)) + return true; + return false; + }); + + return new ArrayList<>(suggestions); + }); + } catch (ExecutionException e) { + Log.severe(e); + return new ArrayList<>(suggestionsKeywordsResultMap.keySet()); + } + + + } + + // TODO sort results + +} diff --git a/src/main/java/fr/pandacube/util/search/SearchResult.java b/src/main/java/fr/pandacube/util/search/SearchResult.java new file mode 100644 index 0000000..fc02b52 --- /dev/null +++ b/src/main/java/fr/pandacube/util/search/SearchResult.java @@ -0,0 +1,11 @@ +package fr.pandacube.util.search; + +import java.util.Set; + +public interface SearchResult { + + public Set getSearchKeywords(); + + public Set getSuggestionKeywords(); + +}