/**
* 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.File;
import java.io.IOException;
import java.util.Date;
import java.util.Set;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.olat.core.commons.services.search.OlatDocument;
import org.olat.core.commons.services.search.QueryException;
import org.olat.core.commons.services.search.SearchResults;
import org.olat.core.commons.services.search.ServiceNotAvailableException;
import org.olat.core.id.Identity;
import org.olat.core.id.Roles;
import org.olat.core.logging.AssertException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.ArrayHelper;
import org.olat.search.service.SearchMetadataFieldsProvider;
import org.olat.search.service.SearchServiceImpl;
import org.olat.search.service.spell.SearchSpellChecker;
/**
* Search part inside of search-service.
* @author Christian Guretzki
*/
public class Search implements OLATSearcher {
private OLog log = Tracing.createLoggerFor(this.getClass());
private String indexPath;
private Analyzer analyzer;
private Searcher searcher;
private SearchSpellChecker searchSpellChecker;
/** Counts number of search queries since last restart. */
private long queryCount = 0;
private Date openIndexDate;
private Object createIndexSearcherLock = new Object();
private long maxIndexTime;
public void setMaxIndexTime(long maxIndexTime) {
this.maxIndexTime = maxIndexTime;
}
private String fields[] = {OlatDocument.TITLE_FIELD_NAME,OlatDocument.DESCRIPTION_FIELD_NAME,OlatDocument.CONTENT_FIELD_NAME,OlatDocument.AUTHOR_FIELD_NAME};
public Search() {
// called by spring.
}
/**
* [used by spring]
*
*/
public void init() {
// called by spring.
}
/**
* [used by spring]
* @param indexPath the absolute file-path to search index directory.
*/
public void setIndexPath(String indexPath) {
this.indexPath = indexPath;
analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);
try {
createIndexSearcher(indexPath);
checkIsIndexUpToDate();
} catch (IOException e) {
log.info("Can not create IndexSearcher at startup");
}
}
/**
* Do search a certain query. The results will be filtered for the identity and roles.
* @param queryString Search query-string.
* @param identity Filter results for this identity (user).
* @param roles Filter results for this roles (role of user).
* @return SearchResults object for this query
*/
public SearchResults doSearch(String queryString, Identity identity, Roles roles, boolean doHighlighting) throws ServiceNotAvailableException, ParseException, QueryException {
try {
if (!existIndex()) {
log.warn("Index does not exist, can't search for queryString: "+queryString);
throw new ServiceNotAvailableException("Index does not exist");
}
synchronized (createIndexSearcherLock) {//o_clusterOK by:fj if service is only configured on one vm, which is recommended way
if (searcher == null) {
try {
createIndexSearcher(indexPath);
checkIsIndexUpToDate();
} catch(IOException ioEx) {
log.warn("Can not create searcher", ioEx);
throw new ServiceNotAvailableException("Index is not available");
}
}
if ( hasNewerIndexFile() ) {
reopenIndexSearcher();
checkIsIndexUpToDate();
}
}
log.info("queryString=" + queryString);
QueryParser queryParser = new MultiFieldQueryParser(Version.LUCENE_CURRENT, fields, analyzer);
queryParser.setLowercaseExpandedTerms(false);//some add. fields are not tokenized and not lowered case
Query query = queryParser.parse(queryString);
try {
query = searcher.rewrite(query);
} catch (Exception ex) {
throw new QueryException("Rewrite-Exception query because too many clauses. Query=" + query);
}
long startTime = System.currentTimeMillis();
int n = SearchServiceImpl.getInstance().getSearchModuleConfig().getMaxHits() + 1;
TopDocs docs = searcher.search(query, n);
long queryTime = System.currentTimeMillis() - startTime;
if (log.isDebug()) log.debug("hits.length()=" + docs.totalHits);
SearchResultsImpl searchResult = new SearchResultsImpl(searcher, docs, query, analyzer, identity, roles, doHighlighting);
searchResult.setQueryTime(queryTime);
searchResult.setNumberOfIndexDocuments(searcher.maxDoc());
queryCount++;
return searchResult;
} catch (ServiceNotAvailableException naex) {
// pass exception
throw new ServiceNotAvailableException(naex.getMessage());
} catch (ParseException pex) {
throw new ParseException("can not parse query=" + queryString);
} catch (QueryException qex) {
throw new QueryException(qex.getMessage());
} catch (Exception ex) {
log.warn("Exception in search", ex);
throw new ServiceNotAvailableException(ex.getMessage());
}
}
private void checkIsIndexUpToDate() throws IOException {
long indexTime = getCurrentIndexDate().getTime();
long currentTime = System.currentTimeMillis();
if ( (currentTime - indexTime ) > maxIndexTime) {
log.error("Search index is too old indexDate=" + getCurrentIndexDate());
}
}
public long getQueryCount() {
return queryCount;
}
public void stop() {
try {
if (searcher != null) {
searcher.close();
searcher = null;
}
} catch (IOException e) {
log.error("", e);
}
}
private boolean hasNewerIndexFile() {
try {
if (getCurrentIndexDate().after(openIndexDate) ) {
return true;
}
} catch (IOException e) { // no index file exist
}
return false;
}
private void createIndexSearcher(String path) throws IOException {
File indexFile = new File(path);
Directory directory = FSDirectory.open(indexFile);
searcher = new IndexSearcher(directory);
openIndexDate = getCurrentIndexDate();
}
private void reopenIndexSearcher() {
if ( hasNewerIndexFile() ) {
log.debug("New index file available, reopen it");
try {
searcher.close();
createIndexSearcher(indexPath);
} catch (IOException e) {
log.warn("Could not reopen index-searcher", e);
}
}
}
/**
* @return Creation date of current used search index.
*/
private Date getCurrentIndexDate() throws IOException {
File indexFile = new File(indexPath);
Directory directory = FSDirectory.open(indexFile);
return new Date(IndexReader.getCurrentVersion(directory));
}
/**
* Check if index exist.
* @return true : Index exists.
*/
private boolean existIndex()
throws IOException {
try {
File indexFile = new File(indexPath);
Directory directory = FSDirectory.open(indexFile);
return IndexReader.indexExists(directory);
} catch (IOException e) {
throw e;
}
}
/**
* Sets the SearchSpellChecker delegate.
* @param searchSpellChecker
*/
public void setSearchSpellChecker(SearchSpellChecker searchSpellChecker) {
this.searchSpellChecker = searchSpellChecker;
}
/**
* Delegates impl to the searchSpellChecker.
* @see org.olat.search.service.searcher.OLATSearcher#spellCheck(java.lang.String)
*/
public Set<String> spellCheck(String query) {
if(searchSpellChecker==null) throw new AssertException ("Try to call spellCheck() in Search.java but searchSpellChecker is null");
return searchSpellChecker.check(query);
}
/**
* Spring setter to inject the available metadata
*
* @param metadataFields
*/
public void setMetadataFields(SearchMetadataFieldsProvider metadataFields) {
if (metadataFields != null) {
// add metadata fields to normal fields
String[] metaFields = ArrayHelper.toArray(metadataFields.getAdvancedSearchableFields());
String[] newFields = new String[this.fields.length + metaFields.length];
System.arraycopy(this.fields, 0, newFields, 0, this.fields.length);
System.arraycopy(metaFields, 0, newFields, this.fields.length, metaFields.length);
this.fields = newFields;
}
}
}