/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br>
* University of Zurich, Switzerland.
* <p>
*/
package org.olat.search.service.searcher;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.persistence.DBFactory;
import org.olat.core.commons.services.search.OlatDocument;
import org.olat.core.commons.services.search.ResultDocument;
import org.olat.core.commons.services.search.SearchResults;
import org.olat.core.id.Identity;
import org.olat.core.id.Roles;
import org.olat.core.id.context.BusinessControl;
import org.olat.core.id.context.BusinessControlFactory;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.search.service.SearchServiceImpl;
import org.olat.search.service.indexer.Indexer;
/**
* Data object to pass search results back from search service.
* @author Christian Guretzki
*
*/
public class SearchResultsImpl implements SearchResults {
private static final OLog log = Tracing.createLoggerFor(SearchResultsImpl.class);
private static final String HIGHLIGHT_PRE_TAG = "<span class=\"o_search_result_highlight\">";
private static final String HIGHLIGHT_POST_TAG = "</span>";
private static final String HIGHLIGHT_SEPARATOR = "...<br />";
/* Define in module config */
private int maxResults;
private long queryTime;
private int numberOfIndexDocuments;
/* List of ResultDocument. */
private List<ResultDocument> resultList;
/**
* Constructure for certain search-results.
* Does not include any search-call to search-service.
* Search call must be made before to create a Hits object.
* @param hits Search hits return from search.
* @param query Search query-string.
* @param analyzer Search analyser, must be the same like at creation of index.
* @param identity Filter results for this idenity (user).
* @param roles Filter results for this roles (role of user).
* @param doHighlighting Flag to enable highlighting search
* @throws IOException
*/
public SearchResultsImpl(Searcher searcher, TopDocs docs, Query query, Analyzer analyzer, Identity identity, Roles roles, boolean doHighlighting) throws IOException{
maxResults = SearchServiceImpl.getInstance().getSearchModuleConfig().getMaxResults();
int maxHits = SearchServiceImpl.getInstance().getSearchModuleConfig().getMaxHits();
List<OlatDocument> olatDocList = createOlatDocumentList(searcher, docs, maxHits);
// filter this list (remove those entries not visible)
List<OlatDocument> filteredList = doFilter(identity, roles, olatDocList, maxResults);
// Convert filteredList to ResultDocument list with highlighting text if nesessary
resultList = convertToResultDocument(query, analyzer, doHighlighting, filteredList);
}
/**
*
* @return Length of result-list.
*/
public String getLength() {
return Integer.toString(resultList.size());
}
/**
* @return List of ResultDocument.
*/
public List<ResultDocument> getList() {
return resultList;
}
/**
* Set query response time in milliseconds.
* @param queryTime Query response time in milliseconds.
*/
public void setQueryTime(long queryTime) {
this.queryTime = queryTime;
}
/**
* @return Query response time in milliseconds.
*/
public String getQueryTime() {
return Long.toString(queryTime);
}
/**
* Set number of search-index-elements.
* @param numberOfIndexDocuments Number of search-index-elements.
*/
public void setNumberOfIndexDocuments(int numberOfIndexDocuments) {
this.numberOfIndexDocuments = numberOfIndexDocuments;
}
/**
* @return Number of search-index-elements.
*/
public String getNumberOfIndexDocuments() {
return Integer.toString(numberOfIndexDocuments);
}
/**
* Check if the search found too many results.
* @return TRUE: search results found too many results.
*/
public boolean hasTooManyResults() {
return (resultList.size() >= maxResults);
}
/**
* @return Number of maximal possible results.
*/
public String getMaxResults() {
return Integer.toString(maxResults);
}
///////////////////
// Private Methods
///////////////////
/**
* Convert filteredList to ResultDocument list with highlighting text if nesessary.
* @param query
* @param analyzer
* @param doHighlighting
* @param filteredList
* @return
* @throws IOException
*/
private List<ResultDocument> convertToResultDocument(Query query, Analyzer analyzer, boolean doHighlighting, List<OlatDocument> filteredList) throws IOException {
List<ResultDocument> newResultList = new ArrayList<ResultDocument>(maxResults);
Iterator<OlatDocument> iter = filteredList.iterator();
while (iter.hasNext()) {
OlatDocument odoc = iter.next();
Document doc = odoc.getLuceneDocument();
ResultDocument resultDocument = new ResultDocument(doc);
if (doHighlighting) {
doHighlight(query, analyzer, doc, resultDocument);
}
newResultList.add(resultDocument);
}
return newResultList;
}
/**
* Highlight (bold,color) query words in result-document. Set HighlightResult for content or description.
* @param query
* @param analyzer
* @param doc
* @param resultDocument
* @throws IOException
*/
private void doHighlight(Query query, Analyzer analyzer, Document doc, ResultDocument resultDocument) throws IOException {
Highlighter highlighter = new Highlighter(new SimpleHTMLFormatter(HIGHLIGHT_PRE_TAG,HIGHLIGHT_POST_TAG) , new QueryScorer(query));
// Get 3 best fragments of content and seperate with a "..."
String content = doc.get(OlatDocument.CONTENT_FIELD_NAME);
String title = doc.get(OlatDocument.TITLE_FIELD_NAME);
Set terms = new HashSet();
query.extractTerms(terms);
try {
//highlight content
TokenStream tokenStream = analyzer.tokenStream(OlatDocument.CONTENT_FIELD_NAME, new StringReader(content));
String highlightResult = highlighter.getBestFragments(tokenStream, content, 3, HIGHLIGHT_SEPARATOR);
// if no highlightResult is in content => look in description
if (highlightResult.length() == 0) {
String description = doc.get(OlatDocument.DESCRIPTION_FIELD_NAME);
tokenStream = analyzer.tokenStream(OlatDocument.DESCRIPTION_FIELD_NAME, new StringReader(description));
highlightResult = highlighter.getBestFragments(tokenStream, description, 3, HIGHLIGHT_SEPARATOR);
resultDocument.setHighlightingDescription(true);
}
resultDocument.setHighlightResult(highlightResult);
//highlight title
tokenStream = analyzer.tokenStream(OlatDocument.CONTENT_FIELD_NAME, new StringReader(title));
String highlightTitle = highlighter.getBestFragments(tokenStream, title, 3, " ");
resultDocument.setHighlightTitle(highlightTitle);
} catch (InvalidTokenOffsetsException e) {
log.warn("", e);
}
}
/**
* Creates a list of OlatDocument based on Hits object
*/
private List<OlatDocument> createOlatDocumentList(Searcher searcher, TopDocs docs, int maxHits) throws CorruptIndexException, IOException {
// build up a list
int numOfDocs = Math.min(maxHits, docs.totalHits);
List<OlatDocument> res = new ArrayList<OlatDocument>(numOfDocs);
for (int i=0; i<numOfDocs; i++) {
Document luDoc = searcher.doc(docs.scoreDocs[i].doc);
res.add(new OlatDocument(luDoc));
}
return res;
}
/**
* Filter search results for certain user and roles.
* @param identity The current identity of the user.
* @param roles The roles of the user.
* @param res Unfiltered list of OlatDocument's
* @return Filtered list of OlatDocument's
*/
private List<OlatDocument> doFilter(Identity identity, Roles roles, List<OlatDocument> unFilteredList, int maxToReturn) {
// check if admin
if (roles.isOLATAdmin()) {
return createResultListForAdmin(unFilteredList, maxToReturn);
}
List<OlatDocument> filteredList = new ArrayList<OlatDocument>(maxToReturn);
long startTime = 0;
if ( log.isDebug() ) {
startTime = System.currentTimeMillis();
}
Indexer mainIndexer = (Indexer)CoreSpringFactory.getBean("mainindexer");
// loop over all results
Iterator<OlatDocument> it_odocs = unFilteredList.iterator();
int resultCount = 0;
int loopCount = 0;
while (it_odocs.hasNext() && resultCount < maxToReturn) {
long elementStartTime = 0;
if ( log.isDebug() ) {
elementStartTime = System.currentTimeMillis();
}
OlatDocument odoc = it_odocs.next();
String resourceUrl = odoc.getResourceUrl();
BusinessControl businessControl = BusinessControlFactory.getInstance().createFromString(resourceUrl);
boolean hasAccess = mainIndexer.checkAccess(null, businessControl, identity, roles);
if (hasAccess) {
filteredList.add(odoc);
resultCount++;
}
loopCount++;
if ( log.isDebug() ) {
long elementEndTime = System.currentTimeMillis();
log.debug("doFilter: elementStartTime[" + resultCount + "]=" + (elementEndTime - elementStartTime) + " bc=(" + resourceUrl + ")");
}
if (loopCount % 10 == 0) {
// Do commit after certain number of documents because the transaction should not be too big
DBFactory.getInstance().intermediateCommit();
log.debug("DB: committed and close session");
}
}
if ( log.isDebug() ) {
log.debug("doFilter: loopCount=" + loopCount);
log.debug("doFilter: resultCount=" + resultCount);
long endTime = System.currentTimeMillis();
log.debug("doFilter: summary time=" + (endTime - startTime));
}
return filteredList;
}
private List<OlatDocument> createResultListForAdmin(List<OlatDocument> unFilteredList, int maxToReturn) {
// Admin has access for everything => Return list unchanged, first check size
if (unFilteredList.size() > maxResults) {
// Too many results => reduce to MAX_RESULTS
return unFilteredList.subList(0, maxToReturn);
}
// list < max => nothing to filter, return un-changed
return unFilteredList;
}
}