/* * Licensed under the Apache License, Version 2.0 (the "License"); * * You may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * limitations under the License. * * Contributions from 2013-2017 where performed either by US government * employees, or under US Veterans Health Administration contracts. * * US Veterans Health Administration contributions by government employees * are work of the U.S. Government and are not subject to copyright * protection in the United States. Portions contributed by government * employees are USGovWork (17USC ยง105). Not subject to copyright. * * Contribution by contractors to the US Veterans Health Administration * during this period are contractually contributed under the * Apache License, Version 2.0. * * See: https://www.usa.gov/government-works * * Contributions prior to 2013: * * Copyright (C) International Health Terminology Standards Development Organisation. * Licensed under the Apache License, Version 2.0. * */ package sh.isaac.provider.query.search; //~--- JDK imports ------------------------------------------------------------ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; //~--- non-JDK imports -------------------------------------------------------- import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sh.isaac.api.Get; import sh.isaac.api.LookupService; import sh.isaac.api.chronicle.ObjectChronology; import sh.isaac.api.component.concept.ConceptChronology; import sh.isaac.api.component.concept.ConceptVersion; import sh.isaac.api.component.sememe.version.DescriptionSememe; import sh.isaac.api.identity.StampedVersion; import sh.isaac.api.index.SearchResult; import sh.isaac.api.util.NumericUtils; import sh.isaac.api.util.TaskCompleteCallback; import sh.isaac.api.util.UUIDUtil; import sh.isaac.MetaData; import sh.isaac.provider.query.lucene.LuceneDescriptionType; import sh.isaac.provider.query.lucene.LuceneIndexer; import sh.isaac.provider.query.lucene.indexers.DescriptionIndexer; import sh.isaac.provider.query.lucene.indexers.SememeIndexer; import sh.isaac.utility.Frills; //~--- classes ---------------------------------------------------------------- /** * Class for handling the ISAAC search functionality. * <p> * A wrapper on top of the search capabilities provided by {@link LuceneIndexer} implementations that adds things like * background threaded searches, convenience methods for common searches, etc. * * @author <a href="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</a> * @author ocarlsen */ //TODO need to rework these APIs to take in path info - so that the path for the search can easily be customized from the search GUI public class SearchHandler { /** The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger(SearchHandler.class); /** The description sememe assemblages cache. */ private static Integer[] descriptionSememeAssemblagesCache = null; //~--- methods ------------------------------------------------------------- /** * Calls {@link #descriptionSearch(String, int, boolean, TaskCompleteCallback, Integer, SearchResultsFilter, Comparator, boolean, Supplier)} * passing false for prefixSearch, null for the taskID, null for the filter, null for the comparator. * * @param query - The query string * @param resultLimit - limit to X results. Use {@link Integer#MAX_VALUE} for no limit. * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param mergeResultsOnConcepts the merge results on concepts * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle descriptionSearch(String query, int resultLimit, Consumer<SearchHandle> operationToRunWhenSearchComplete, boolean mergeResultsOnConcepts, boolean includeOffPathResults) { return descriptionSearch(query, resultLimit, false, operationToRunWhenSearchComplete, (Integer) null, null, null, mergeResultsOnConcepts, includeOffPathResults); } /** * ** ADVANCED API** - you probably don't want this method.... * * This is really just a convenience wrapper with threading and results conversion on top of the APIs available in {@link DescriptionIndexer} * * Execute a Query against the description indexes in a background thread, hand back a handle to the search object which will * allow you to get the results (when they are ready) and also cancel an in-progress query. * * If there is a problem with the internal indexes - an error will be logged, and the exception will be re-thrown when the * {@link SearchHandle#getResults()} method of the SearchHandle is called. * * @param query - The query string * @param searchFunction - A function that will call one of the query(...) methods within {@link DescriptionIndexer}. See * that class for documentation on the various search types supported. * @param prefixSearch - true to use the "prefex search" algorithm. False to use the standard lucene algorithm. * See {@link DescriptionIndexer#query(String, boolean, ComponentProperty, int, Long)} for more details on this algorithm. * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param taskId - An optional field that is simply handed back during the callback when results are complete. Useful for matching * requests to this method with callbacks. * @param filter - An optional filter than can add or remove items from the tentative result set before it is returned. * @param comparator - The comparator to use for sorting the results - optional - uses {@link CompositeSearchResultComparator} if none is * provided. * @param mergeOnConcepts - If true, when multiple description objects within the same concept match the search, this will be returned * as a single result representing the concept - with each matching string listed, and the score being the best score of any of the * matching strings. When false, you will get one search result per description match - so concepts can be returned multiple times. * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle descriptionSearch(String query, final BiFunction<DescriptionIndexer, String, List<SearchResult>> searchFunction, final boolean prefixSearch, final Consumer<SearchHandle> operationToRunWhenSearchComplete, final Integer taskId, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) { final SearchHandle searchHandle = new SearchHandle(taskId); if (!prefixSearch) { // Just strip out parens, which are common in FSNs, but also lucene search operators (which our users likely won't use) query = query.replaceAll("\\(", ""); query = query.replaceAll("\\)", ""); } final String localQuery = query; // Do search in background. final Runnable r = () -> { final List<CompositeSearchResult> initialSearchResults = new ArrayList<>(); try { if (localQuery.length() > 0) { // If search query is an ID, look up concept and add the result. if (UUIDUtil.isUUID(localQuery) || NumericUtils.isLong(localQuery)) { final Optional<? extends ConceptChronology<? extends ConceptVersion<?>>> temp = Frills.getConceptForUnknownIdentifier(localQuery); if (temp.isPresent()) { final CompositeSearchResult gsr = new CompositeSearchResult(temp.get(), 2.0f); initialSearchResults.add(gsr); } } LOG.debug("Lucene Search: '" + localQuery + "'"); final DescriptionIndexer descriptionIndexer = LookupService.getService(DescriptionIndexer.class); if (descriptionIndexer == null) { LOG.warn("No description indexer found, aborting."); searchHandle.setError( new Exception("No description indexer available, cannot search")); } else { // Look for description matches. final List<SearchResult> searchResults = searchFunction.apply(descriptionIndexer, localQuery); final int resultCount = searchResults.size(); LOG.debug(resultCount + " results"); if (resultCount > 0) { // Compute the max score of all results. float maxScore = 0.0f; for (final SearchResult searchResult1: searchResults) { final float score = searchResult1.getScore(); if (score > maxScore) { maxScore = score; } } for (final SearchResult searchResult2: searchResults) { // Abort if search has been cancelled. if (searchHandle.isCancelled()) { break; } // Get the description object. final Optional<? extends ObjectChronology<? extends StampedVersion>> io = Get.identifiedObjectService() .getIdentifiedObjectChronology(searchResult2.getNid()); // normalize the scores between 0 and 1 final float normScore = (searchResult2.getScore() / maxScore); final CompositeSearchResult csr = (io.isPresent() ? new CompositeSearchResult(io.get(), normScore) : new CompositeSearchResult(searchResult2.getNid(), normScore)); initialSearchResults.add(csr); // add one to the scores when we are doing a prefix search, and it hits. if (prefixSearch && (csr.getBestScore() <= 1.0f) && io.isPresent() && (io.get() instanceof DescriptionSememe<?>)) { final String matchingString = ((DescriptionSememe<?>) io.get()).getText(); float adjustValue = 0f; if (matchingString.toLowerCase(Locale.ENGLISH) .equals(localQuery.trim() .toLowerCase(Locale.ENGLISH))) { // "exact match, bump by 2" adjustValue = 2.0f; } else if (matchingString.toLowerCase(Locale.ENGLISH) .startsWith(localQuery.trim() .toLowerCase(Locale.ENGLISH))) { // "add 1, plus a bit more boost based on the length of the matches (shorter matches get more boost)" adjustValue = 1.0f + (1.0f - ((float) (matchingString.length() - localQuery.trim().length()) / (float) matchingString.length())); } if (adjustValue > 0f) { csr.adjustScore(csr.getBestScore() + adjustValue); } } } } } } // sort, filter and merge the results as necessary processResults(searchHandle, initialSearchResults, filter, comparator, mergeOnConcepts, includeOffPathResults); } catch (final Exception ex) { LOG.error("Unexpected error during lucene search", ex); searchHandle.setError(ex); } if (operationToRunWhenSearchComplete != null) { operationToRunWhenSearchComplete.accept(searchHandle); } }; Get.workExecutors() .getExecutor() .execute(r); return searchHandle; } /** * Execute a Query against the description indexes in a background thread, hand back a handle to the search object which will * allow you to get the results (when they are ready) and also cancel an in-progress query. * * If there is a problem with the internal indexes - an error will be logged, and the exception will be re-thrown when the * {@link SearchHandle#getResults()} method of the SearchHandle is called. * * @param query - The query string * @param resultLimit - limit to X results. Use {@link Integer#MAX_VALUE} for no limit. * @param prefixSearch - true to use the "prefex search" algorithm. False to use the standard lucene algorithm. * See {@link DescriptionIndexer#query(String, boolean, ComponentProperty, int, Long)} for more details on this algorithm. * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param taskId - An optional field that is simply handed back during the callback when results are complete. Useful for matching * requests to this method with callbacks. * @param filter - An optional filter than can add or remove items from the tentative result set before it is returned. * @param comparator - The comparator to use for sorting the results - optional - uses {@link CompositeSearchResultComparator} if none is * provided. * @param mergeOnConcepts - If true, when multiple description objects within the same concept match the search, this will be returned * as a single result representing the concept - with each matching string listed, and the score being the best score of any of the * matching strings. When false, you will get one search result per description match - so concepts can be returned multiple times. * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle descriptionSearch(String query, final int resultLimit, final boolean prefixSearch, Consumer<SearchHandle> operationToRunWhenSearchComplete, final Integer taskId, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) { return descriptionSearch(query, (index, queryString) -> { try { return index.query(queryString, prefixSearch, getDescriptionSememeAssemblages(), resultLimit, Long.MIN_VALUE); } catch (final Exception e) { throw new RuntimeException(e); } }, prefixSearch, operationToRunWhenSearchComplete, taskId, filter, comparator, mergeOnConcepts, includeOffPathResults); } /** * Execute a Query against a specific type of description in a background thread, hand back a handle to the search object which will * allow you to get the results (when they are ready) and also cancel an in-progress query. * * If there is a problem with the internal indexes - an error will be logged, and the exception will be re-thrown when the * {@link SearchHandle#getResults()} method of the SearchHandle is called. * * @param query - The query text * @param resultLimit - limit to X results. Use {@link Integer#MAX_VALUE} for no limit. * @param prefixSearch - true to use the "prefex search" algorithm. False to use the standard lucene algorithm. * See {@link DescriptionIndexer#query(String, boolean, Integer, int, Long)} for more details on this algorithm. * @param descriptionType - The type to search within (FSN / Synonym / Description) * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param taskId - An optional field that is simply handed back during the callback when results are complete. Useful for matching * requests to this method with callbacks. * @param filter - An optional filter than can add or remove items from the tentative result set before it is returned. * @param comparator - The comparator to use for sorting the results - optional - uses {@link CompositeSearchResultComparator} if none is * provided. * @param mergeOnConcepts - If true, when multiple description objects within the same concept match the search, this will be returned * as a single result representing the concept - with each matching string listed, and the score being the best score of any of the * matching strings. When false, you will get one search result per description match - so concepts can be returned multiple times. * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle descriptionSearch(String query, final int resultLimit, final boolean prefixSearch, final LuceneDescriptionType descriptionType, Consumer<SearchHandle> operationToRunWhenSearchComplete, final Integer taskId, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) { return descriptionSearch(query, (index, queryString) -> { try { return index.query(queryString, descriptionType, resultLimit, Long.MIN_VALUE); } catch (final Exception e) { throw new RuntimeException(e); } }, prefixSearch, operationToRunWhenSearchComplete, taskId, filter, comparator, mergeOnConcepts, includeOffPathResults); } /** * Execute a Query against a specific type of description in a background thread, hand back a handle to the search object which will * allow you to get the results (when they are ready) and also cancel an in-progress query. * * If there is a problem with the internal indexes - an error will be logged, and the exception will be re-thrown when the * {@link SearchHandle#getResults()} method of the SearchHandle is called. * * @param query - The query text * @param resultLimit - limit to X results. Use {@link Integer#MAX_VALUE} for no limit. * @param prefixSearch - true to use the "prefex search" algorithm. False to use the standard lucene algorithm. * See {@link DescriptionIndexer#query(String, boolean, ComponentProperty, int, Long)} for more details on this algorithm. * @param extendedDescriptionType - The UUID of the concept that represents the extended (terminology specific) description type. * This concept should be a child of {@link IsaacMetadataAuxiliaryBinding#DESCRIPTION_TYPE_IN_SOURCE_TERMINOLOGY} * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param taskId - An optional field that is simply handed back during the callback when results are complete. Useful for matching * requests to this method with callbacks. * @param filter - An optional filter than can add or remove items from the tentative result set before it is returned. * @param comparator - The comparator to use for sorting the results - optional - uses {@link CompositeSearchResultComparator} if none is * provided. * @param mergeOnConcepts - If true, when multiple description objects within the same concept match the search, this will be returned * as a single result representing the concept - with each matching string listed, and the score being the best score of any of the * matching strings. When false, you will get one search result per description match - so concepts can be returned multiple times. * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle descriptionSearch(String query, final int resultLimit, final boolean prefixSearch, final UUID extendedDescriptionType, Consumer<SearchHandle> operationToRunWhenSearchComplete, final Integer taskId, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) { return descriptionSearch(query, (index, queryString) -> { try { return index.query(queryString, extendedDescriptionType, resultLimit, Long.MIN_VALUE); } catch (final Exception e) { throw new RuntimeException(e); } }, prefixSearch, operationToRunWhenSearchComplete, taskId, filter, comparator, mergeOnConcepts, includeOffPathResults); } /** * Execute a Query against the sememe indexes in a background thread, hand back a handle to the search object which will * allow you to get the results (when they are ready) and also cancel an in-progress query. * * If there is a problem with the internal indexes - an error will be logged, and the exception will be re-thrown when the * {@link SearchHandle#getResults()} method of the SearchHandle is called. * * This is really just a convenience wrapper with threading and results conversion on top of the APIs available in {@link SememeIndexer} * * @param searchFunction A function that will call one of the query(...) methods within {@link SememeIndexer}. See * that class for documentation on the various search types supported. * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param taskId - An optional field that is simply handed back during the callback when results are complete. Useful for matching * requests to this method with callbacks. * @param filter - An optional filter than can add or remove items from the tentative result set before it is returned. * @param comparator - The comparator to use for sorting the results * @param mergeOnConcepts - If true, when multiple description objects within the same concept match the search, this will be returned * as a single result representing the concept - with each matching string listed, and the score being the best score of any of the * matching strings. When false, you will get one search result per description match - so concepts can be returned multiple times. * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle sememeSearch(Function<SememeIndexer, List<SearchResult>> searchFunction, final Consumer<SearchHandle> operationToRunWhenSearchComplete, final Integer taskId, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) { final SearchHandle searchHandle = new SearchHandle(taskId); // Do search in background. final Runnable r = () -> { try { List<CompositeSearchResult> initialSearchResults = new ArrayList<>(); final SememeIndexer refexIndexer = LookupService.getService(SememeIndexer.class); if (refexIndexer == null) { LOG.warn("No sememe indexer found, aborting."); searchHandle.setError(new Exception("No sememe indexer available, cannot search")); } else { final List<SearchResult> searchResults = searchFunction.apply(refexIndexer); LOG.debug(searchResults.size() + " results"); initialSearchResults = normalizeScores(searchResults, searchHandle); } // sort, filter and merge the results as necessary processResults(searchHandle, initialSearchResults, filter, comparator, mergeOnConcepts, includeOffPathResults); } catch (final Exception ex) { LOG.error("Unexpected error during lucene search", ex); searchHandle.setError(ex); } if (operationToRunWhenSearchComplete != null) { operationToRunWhenSearchComplete.accept(searchHandle); } }; Get.workExecutors() .getExecutor() .execute(r); return searchHandle; } /** * A convenience wrapper for {@link #sememeSearch(Function, TaskCompleteCallback, Integer, Function, Comparator, boolean)} * which builds a function that handles basic string searches. * * @param searchString - The value to search for within the refex index * @param resultLimit - cap the number of results * @param prefixSearch - use the prefix search text algorithm. See {@link DescriptionIndexer#query(String, boolean, Integer[], int, Long)} for * a description on the prefix search algorithm. * @param assemblageNid - (optional) limit the search to the specified assemblage type, or all assemblage objects (if null) * @param operationToRunWhenSearchComplete - (optional) Pass the function that you want to have executed when the search is complete and the results * are ready for use. Note that this function will also be executed in the background thread. * @param taskId - An optional field that is simply handed back during the callback when results are complete. Useful for matching * requests to this method with callbacks. * @param filter - An optional filter than can add or remove items from the tentative result set before it is returned. * @param comparator - The comparator to use for sorting the results * @param mergeOnConcepts - If true, when multiple description objects within the same concept match the search, this will be returned * as a single result representing the concept - with each matching string listed, and the score being the best score of any of the * matching strings. When false, you will get one search result per description match - so concepts can be returned multiple times. * @param includeOffPathResults - true to give back results for all hits (which may have unresolvable concepts) or false to filter those * out that are not on THE CURRENT COORDINATE configuration * @return A handle to the running search. */ public static SearchHandle sememeSearch(String searchString, int resultLimit, boolean prefixSearch, Integer assemblageNid, Consumer<SearchHandle> operationToRunWhenSearchComplete, final Integer taskId, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) { return sememeSearch(index -> { try { return index.query(searchString, prefixSearch, ((assemblageNid == null) ? (Integer[]) null : new Integer[] { assemblageNid }), resultLimit, Long.MIN_VALUE); } catch (final Exception e) { throw new RuntimeException(e); } }, operationToRunWhenSearchComplete, taskId, filter, comparator, mergeOnConcepts, includeOffPathResults); } /** * Normalize scores. * * @param searchResults the search results * @param searchHandle the search handle * @return the list */ private static List<CompositeSearchResult> normalizeScores(List<SearchResult> searchResults, SearchHandle searchHandle) { final List<CompositeSearchResult> initialSearchResults = new ArrayList<>(); if (searchResults.size() > 0) { // Compute the max score of all results. float maxScore = 0.0f; for (final SearchResult searchResult: searchResults) { final float score = searchResult.getScore(); if (score > maxScore) { maxScore = score; } } for (final SearchResult searchResult: searchResults) { // Abort if search has been cancelled. if (searchHandle.isCancelled()) { break; } // Get the match object. final Optional<? extends ObjectChronology<? extends StampedVersion>> io = Get.identifiedObjectService() .getIdentifiedObjectChronology( searchResult.getNid()); // normalize the scores between 0 and 1 final float normScore = (searchResult.getScore() / maxScore); final CompositeSearchResult csr = (io.isPresent() ? new CompositeSearchResult(io.get(), normScore) : new CompositeSearchResult(searchResult.getNid(), normScore)); initialSearchResults.add(csr); } } return initialSearchResults; } /** * Process results. * * @param searchHandle the search handle * @param rawResults the raw results * @param filter the filter * @param comparator the comparator * @param mergeOnConcepts the merge on concepts * @param includeOffPathResults the include off path results * @throws SearchResultsFilterException the search results filter exception */ private static void processResults(SearchHandle searchHandle, List<CompositeSearchResult> rawResults, final Function<List<CompositeSearchResult>, List<CompositeSearchResult>> filter, Comparator<CompositeSearchResult> comparator, boolean mergeOnConcepts, boolean includeOffPathResults) throws SearchResultsFilterException { // filter and sort the results if (filter != null) { LOG.debug("Applying SearchResultsFilter " + filter + " to " + rawResults.size() + " search results"); rawResults = filter.apply(rawResults); LOG.debug(rawResults.size() + " results remained after running the filter"); } if (!includeOffPathResults) { LOG.debug("Applying Path filter to " + rawResults.size() + " search results"); rawResults = new Function<List<CompositeSearchResult>, List<CompositeSearchResult>>() { @Override public List<CompositeSearchResult> apply(List<CompositeSearchResult> t) { final Iterator<CompositeSearchResult> it = t.iterator(); while (it.hasNext()) { if (!it.next() .getContainingConcept() .isPresent()) { it.remove(); } } return t; } }.apply(rawResults); LOG.debug(rawResults.size() + " results remained after running path filter"); } if (mergeOnConcepts) { final Hashtable<Integer, CompositeSearchResult> merged = new Hashtable<>(); final ArrayList<CompositeSearchResult> unmergeable = new ArrayList<>(); for (final CompositeSearchResult csr: rawResults) { if (!csr.getContainingConcept() .isPresent()) { unmergeable.add(csr); continue; } final CompositeSearchResult found = merged.get(csr.getContainingConcept() .get() .getNid()); if (found == null) { merged.put(csr.getContainingConcept() .get() .getNid(), csr); } else { found.merge(csr); } } rawResults.clear(); rawResults.addAll(merged.values()); rawResults.addAll(unmergeable); } Collections.sort(rawResults, ((comparator == null) ? new CompositeSearchResultComparator() : comparator)); searchHandle.setResults(rawResults); } //~--- get methods --------------------------------------------------------- /** * Gets the description sememe assemblages. * * @return the description sememe assemblages */ private static Integer[] getDescriptionSememeAssemblages() { if (descriptionSememeAssemblagesCache == null) { final Set<Integer> descSememes = Frills.getAllChildrenOfConcept(MetaData.DESCRIPTION_ASSEMBLAGE.getConceptSequence(), true, false); descSememes.add(MetaData.DESCRIPTION_ASSEMBLAGE.getConceptSequence()); descriptionSememeAssemblagesCache = descSememes.toArray(new Integer[descSememes.size()]); } return descriptionSememeAssemblagesCache; } }