/**
* 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.
* <hr>
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* This file has been modified by the OpenOLAT community. Changes are licensed
* under the Apache 2.0 license as the original file.
*/
package org.olat.search.service.searcher;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
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.search.IndexSearcher;
import org.apache.lucene.search.Query;
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.SimpleHTMLEncoder;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.olat.core.commons.persistence.DBFactory;
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.core.util.Formatter;
import org.olat.core.util.StringHelper;
import org.olat.core.util.filter.FilterFactory;
import org.olat.search.SearchResults;
import org.olat.search.model.AbstractOlatDocument;
import org.olat.search.model.ResultDocument;
import org.olat.search.service.SearchServiceFactory;
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 long serialVersionUID = 3950063141792217522L;
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 maxHits;
private int totalHits;
private int totalDocs;
private long queryTime;
private int numberOfIndexDocuments;
/* List of ResultDocument. */
private List<ResultDocument> resultList;
private transient Indexer mainIndexer;
/**
* 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 identity (user).
* @param roles Filter results for this roles (role of user).
* @param doHighlighting Flag to enable highlighting search
* @throws IOException
*/
public SearchResultsImpl(Indexer mainIndexer, IndexSearcher searcher, TopDocs docs, Query query, Analyzer analyzer, Identity identity,
Roles roles, int firstResult, int maxReturns, boolean doHighlighting, boolean onlyDbKeys)
throws IOException {
this.mainIndexer = mainIndexer;
resultList = initResultList(identity, roles, query, analyzer, searcher, docs, firstResult, maxReturns, doHighlighting, onlyDbKeys);
}
/**
*
* @return Length of result-list.
*/
@Override
public int size() {
return resultList == null ? 0 : resultList.size();
}
/**
* @return List of ResultDocument.
*/
@Override
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);
}
/**
* @return Number of maximal possible results.
*/
@Override
public int getTotalHits() {
return totalHits;
}
public int getTotalDocs() {
return totalDocs;
}
public String getMaxHits() {
return Integer.toString(maxHits);
}
public boolean hasTooManyResults() {
return totalHits > maxHits;
}
private List<ResultDocument> initResultList(Identity identity, Roles roles, Query query, Analyzer analyzer, IndexSearcher searcher, TopDocs docs,
int firstResult, int maxReturns, final boolean doHighlight, boolean onlyDbKeys)
throws IOException {
Set<String> fields = AbstractOlatDocument.getFields();
if(onlyDbKeys) {
fields.clear();
fields.add(AbstractOlatDocument.DB_ID_NAME);
} else if(!doHighlight) {
fields.remove(AbstractOlatDocument.CONTENT_FIELD_NAME);
}
maxHits = SearchServiceFactory.getService().getSearchModuleConfig().getMaxHits();
totalHits = docs.totalHits;
totalDocs = (docs.scoreDocs == null ? 0 : docs.scoreDocs.length);
int numOfDocs = Math.min(maxHits, docs.totalHits);
List<ResultDocument> res = new ArrayList<ResultDocument>(maxReturns + 1);
for (int i=firstResult; i<numOfDocs && res.size() < maxReturns; i++) {
Document doc;
if(doHighlight) {
doc = searcher.doc(docs.scoreDocs[i].doc);
} else {
doc = searcher.doc(docs.scoreDocs[i].doc, fields);
}
String reservedTo = doc.get(AbstractOlatDocument.RESERVED_TO);
if(StringHelper.containsNonWhitespace(reservedTo) && !"public".equals(reservedTo)
&& !reservedTo.contains(identity.getKey().toString())) {
continue;//admin cannot see private documents
}
ResultDocument rDoc = createResultDocument(doc, i, query, analyzer, doHighlight, identity, roles);
if(rDoc != null) {
res.add(rDoc);
}
if(!roles.isOLATAdmin() && i % 10 == 0) {
// Do commit after certain number of documents because the transaction should not be too big
DBFactory.getInstance().commitAndCloseSession();
}
}
return res;
}
/**
* Create a result document. Return null if the identity has not enough privileges to see the document.
* @param doc
* @param query
* @param analyzer
* @param doHighlight
* @param identity
* @param roles
* @return
* @throws IOException
*/
private ResultDocument createResultDocument(Document doc, int pos, Query query, Analyzer analyzer, boolean doHighlight, Identity identity, Roles roles)
throws IOException {
boolean hasAccess = false;
if(roles.isOLATAdmin()) {
hasAccess = true;
} else {
String resourceUrl = doc.get(AbstractOlatDocument.RESOURCEURL_FIELD_NAME);
if(resourceUrl == null) {
resourceUrl = "";
}
BusinessControl businessControl = BusinessControlFactory.getInstance().createFromString(resourceUrl);
hasAccess = mainIndexer.checkAccess(null, businessControl, identity, roles);
}
ResultDocument resultDoc;
if(hasAccess) {
resultDoc = new ResultDocument(doc, pos);
if (doHighlight) {
doHighlight(query, analyzer, doc, resultDoc);
}
} else {
resultDoc = null;
}
return resultDoc;
}
/**
* 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 SimpleHTMLEncoder(), new QueryScorer(query));
// Get 3 best fragments of content and seperate with a "..."
try {
//highlight content
String content = doc.get(AbstractOlatDocument.CONTENT_FIELD_NAME);
TokenStream tokenStream = analyzer.tokenStream(AbstractOlatDocument.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(AbstractOlatDocument.DESCRIPTION_FIELD_NAME);
tokenStream = analyzer.tokenStream(AbstractOlatDocument.DESCRIPTION_FIELD_NAME, new StringReader(description));
highlightResult = highlighter.getBestFragments(tokenStream, description, 3, HIGHLIGHT_SEPARATOR);
resultDocument.setHighlightingDescription(true);
}
resultDocument.setHighlightResult(highlightResult);
//highlight title
String title = doc.get(AbstractOlatDocument.TITLE_FIELD_NAME);
title = title.trim();
if(title.length() > 128) {
title = FilterFactory.getHtmlTagAndDescapingFilter().filter(title);
title = Formatter.truncate(title, 128);
}
tokenStream = analyzer.tokenStream(AbstractOlatDocument.TITLE_FIELD_NAME, new StringReader(title));
String highlightTitle = highlighter.getBestFragments(tokenStream, title, 3, " ");
resultDocument.setHighlightTitle(highlightTitle);
} catch (InvalidTokenOffsetsException e) {
log.warn("", e);
}
}
}