Simple generic search engine
This commit is contained in:
parent
9a945c950e
commit
45738ddbb8
178
src/main/java/fr/pandacube/util/search/SearchEngine.java
Normal file
178
src/main/java/fr/pandacube/util/search/SearchEngine.java
Normal file
@ -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<R extends SearchResult> {
|
||||||
|
|
||||||
|
Map<String, Set<R>> searchKeywordsResultMap = new HashMap<>();
|
||||||
|
Map<R, Set<String>> resultsSearchKeywordsMap = new HashMap<>();
|
||||||
|
|
||||||
|
Map<String, Set<R>> suggestionsKeywordsResultMap = new HashMap<>();
|
||||||
|
Map<R, Set<String>> resultsSuggestionsKeywordsMap = new HashMap<>();
|
||||||
|
|
||||||
|
Set<R> resultSet = new HashSet<>();
|
||||||
|
|
||||||
|
private Cache<Set<String>, List<String>> 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<String> 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<String> 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<String> searchKw = resultsSearchKeywordsMap.remove(result);
|
||||||
|
for (String skw : searchKw) {
|
||||||
|
Set<R> set = searchKeywordsResultMap.get(skw);
|
||||||
|
set.remove(result);
|
||||||
|
if (set.isEmpty())
|
||||||
|
searchKeywordsResultMap.remove(skw);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> suggestsKw = resultsSearchKeywordsMap.remove(result);
|
||||||
|
for (String skw : suggestsKw) {
|
||||||
|
Set<R> set = suggestionsKeywordsResultMap.get(skw);
|
||||||
|
set.remove(result);
|
||||||
|
if (set.isEmpty())
|
||||||
|
suggestionsKeywordsResultMap.remove(skw);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsSuggestionsKeywordsMap.remove(result);
|
||||||
|
|
||||||
|
suggestionsCache.invalidateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Set<R> search(Set<String> searchTerms) {
|
||||||
|
if (searchTerms == null)
|
||||||
|
searchTerms = new HashSet<String>();
|
||||||
|
|
||||||
|
Set<R> retainedResults = new HashSet<>(resultSet);
|
||||||
|
for (String term : searchTerms) {
|
||||||
|
retainedResults.retainAll(search(term));
|
||||||
|
}
|
||||||
|
|
||||||
|
return retainedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Set<R> search(String searchTerm) {
|
||||||
|
if (searchTerm == null || searchTerm.isEmpty()) {
|
||||||
|
return new HashSet<>(resultSet);
|
||||||
|
}
|
||||||
|
searchTerm = searchTerm.toLowerCase();
|
||||||
|
Set<R> retainedResults = new HashSet<>();
|
||||||
|
for (String skw : searchKeywordsResultMap.keySet()) {
|
||||||
|
if (skw.contains(searchTerm)) {
|
||||||
|
retainedResults.addAll(new ArrayList<>(searchKeywordsResultMap.get(skw)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retainedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<String> suggestKeywords(List<String> prevSearchTerms) {
|
||||||
|
if (prevSearchTerms == null || prevSearchTerms.isEmpty()) {
|
||||||
|
return new ArrayList<>(suggestionsKeywordsResultMap.keySet());
|
||||||
|
}
|
||||||
|
Set<String> lowerCaseSearchTerm = prevSearchTerms.stream()
|
||||||
|
.map(String::toLowerCase)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return suggestionsCache.get(lowerCaseSearchTerm, (Callable<List<String>>) () -> {
|
||||||
|
Set<R> prevResults = search(lowerCaseSearchTerm);
|
||||||
|
|
||||||
|
Set<String> 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
|
||||||
|
|
||||||
|
}
|
11
src/main/java/fr/pandacube/util/search/SearchResult.java
Normal file
11
src/main/java/fr/pandacube/util/search/SearchResult.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package fr.pandacube.util.search;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public interface SearchResult {
|
||||||
|
|
||||||
|
public Set<String> getSearchKeywords();
|
||||||
|
|
||||||
|
public Set<String> getSuggestionKeywords();
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user