/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <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 the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <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> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.search.service.indexer; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.ObjectMessage; import javax.jms.Queue; import javax.jms.QueueConnection; import javax.jms.QueueSender; import javax.jms.QueueSession; import javax.jms.Session; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.index.LogDocMergePolicy; import org.apache.lucene.index.LogMergePolicy; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.olat.core.commons.persistence.DBFactory; import org.olat.core.configuration.ConfigOnOff; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.search.SearchModule; import org.olat.search.SearchService; import org.olat.search.model.AbstractOlatDocument; /** * TODO or not: to make the Indexer cluster wide functional. It would be * possible to create on the fly an IndexWriter with a doInSync. * * Initial date: 04.03.2013<br> * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com * */ public class JmsIndexer implements MessageListener, LifeFullIndexer, ConfigOnOff { private static final int INDEX_MERGE_FACTOR = 1000; private static final OLog log = Tracing.createLoggerFor(JmsIndexer.class); private Queue jmsQueue; private Session indexerSession; private MessageConsumer consumer; private ConnectionFactory connectionFactory; private QueueConnection connection; private String enabled; private CoordinatorManager coordinatorManager; private String permanentIndexPath; private DirectoryReader reader; private IndexWriterHolder permanentIndexWriter; private double ramBufferSizeMB; private boolean indexingNode; private List<LifeIndexer> indexers = new ArrayList<LifeIndexer>(); public JmsIndexer(SearchModule searchModuleConfig, CoordinatorManager coordinatorManager) { indexingNode = searchModuleConfig.isSearchServiceEnabled(); ramBufferSizeMB = searchModuleConfig.getRAMBufferSizeMB(); permanentIndexPath = searchModuleConfig.getFullPermanentIndexPath(); this.coordinatorManager = coordinatorManager; } public Queue getJmsQueue() { return jmsQueue; } /** * [Used by Spring] * @param jmsQueue */ public void setJmsQueue(Queue jmsQueue) { this.jmsQueue = jmsQueue; } /** * [Used by Spring] * @param connectionFactory */ public void setConnectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } /** * [used by Spring] * @param searchService */ public void setSearchServiceEnabled(String enabled) { this.enabled = enabled; } @Override public boolean isEnabled() { return enabled != null && "enabled".equalsIgnoreCase(enabled); } public void setIndexers(List<LifeIndexer> indexers) { if(indexers != null) { for(LifeIndexer indexer:indexers){ addIndexer(indexer); } } } @Override public void addIndexer(LifeIndexer indexer) { indexers.add(indexer); } public List<LifeIndexer> getIndexerByType(String type) { List<LifeIndexer> indexerByType = new ArrayList<LifeIndexer>(); for(LifeIndexer indexer:indexers) { if(type.equals(indexer.getSupportedTypeName())) { indexerByType.add(indexer); } } return indexerByType; } /** * [used by Spring] * @throws JMSException */ public void springInit() throws JMSException { initDirectory(); initQueue(); } public void initQueue() throws JMSException { connection = (QueueConnection)connectionFactory.createConnection(); connection.start(); log.info("springInit: JMS connection started with connectionFactory=" + connectionFactory); if(indexingNode) { //listen to the queue only if indexing node indexerSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); consumer = indexerSession.createConsumer(jmsQueue); consumer.setMessageListener(this); } } public void initDirectory() { try { File tempIndexDir = new File(permanentIndexPath); Directory indexPath = FSDirectory.open(tempIndexDir); if(indexingNode) { permanentIndexWriter = new IndexWriterHolder(indexPath, this); boolean created = permanentIndexWriter.ensureIndexExists(); if(created) { IndexerEvent event = new IndexerEvent(IndexerEvent.INDEX_CREATED); coordinatorManager.getCoordinator().getEventBus().fireEventToListenersOf(event, IndexerEvent.INDEX_ORES); } } reader = DirectoryReader.open(indexPath); } catch (IOException e) { log.error("", e); } } public LogMergePolicy newLogMergePolicy() { LogMergePolicy logmp = new LogDocMergePolicy(); logmp.setCalibrateSizeByDeletes(true); logmp.setMergeFactor(INDEX_MERGE_FACTOR); return logmp; } public IndexWriterConfig newIndexWriterConfig() { Analyzer analyzer = new StandardAnalyzer(SearchService.OO_LUCENE_VERSION); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(SearchService.OO_LUCENE_VERSION, analyzer); indexWriterConfig.setMergePolicy(newLogMergePolicy()); indexWriterConfig.setRAMBufferSizeMB(ramBufferSizeMB);// for better performance set to 48MB (see lucene docu 'how to make indexing faster") indexWriterConfig.setOpenMode(OpenMode.CREATE_OR_APPEND); return indexWriterConfig; } /** * [used by Spring] */ public void stop() { closeQueue(); closeWriter(); } public void closeQueue() { if(consumer != null) { try { consumer.close(); } catch (JMSException e) { log.error("", e); } } if(connection != null) { try { indexerSession.close(); connection.close(); } catch (JMSException e) { log.error("", e); } } } public void closeWriter() { try { permanentIndexWriter.close(); } catch (Exception e) { log.error("", e); } } @Override public void fullIndex() { for(LifeIndexer indexer:indexers) { indexer.fullIndex(this); } } @Override public void indexDocument(String type, Long key) { QueueSender sender; QueueSession session; try { JmsIndexWork workUnit = new JmsIndexWork(JmsIndexWork.INDEX, type, key); session = connection.createQueueSession(false, QueueSession.AUTO_ACKNOWLEDGE ); ObjectMessage message = session.createObjectMessage(); message.setObject(workUnit); sender = session.createSender(getJmsQueue()); sender.send( message ); session.close(); } catch (JMSException e) { log.error("", e ); } } @Override public void indexDocument(String type, List<Long> keyList) { QueueSender sender; QueueSession session; try { JmsIndexWork workUnit = new JmsIndexWork(JmsIndexWork.INDEX, type, keyList); session = connection.createQueueSession(false, QueueSession.AUTO_ACKNOWLEDGE ); ObjectMessage message = session.createObjectMessage(); message.setObject(workUnit); sender = session.createSender(getJmsQueue()); sender.send( message ); session.close(); } catch (JMSException e) { log.error("", e ); } } @Override public void deleteDocument(String type, Long key) { indexDocument(type, Collections.singletonList(key)); } @Override public void onMessage(Message message) { if(message instanceof ObjectMessage) { try { ObjectMessage objMsg = (ObjectMessage)message; JmsIndexWork workUnit = (JmsIndexWork)objMsg.getObject(); if(JmsIndexWork.INDEX.equals(workUnit.getAction())) { doIndex(workUnit); } else if(JmsIndexWork.DELETE.equals(workUnit.getAction())) { doDelete(workUnit); } message.acknowledge(); } catch (JMSException e) { log.error("", e); } finally { DBFactory.getInstance().commitAndCloseSession(); } } } private void doIndex(JmsIndexWork workUnit) { if(isEnabled()) { String type = workUnit.getIndexType(); List<LifeIndexer> lifeIndexers = getIndexerByType(type); for(LifeIndexer indexer:lifeIndexers) { indexer.indexDocument(workUnit.getKeyList(), this); } } } private void doDelete(JmsIndexWork workUnit) { if(isEnabled()) { String type = workUnit.getIndexType(); List<LifeIndexer> lifeIndexers = getIndexerByType(type); for(LifeIndexer indexer:lifeIndexers) { if(workUnit.getKeyList() != null && workUnit.getKeyList().size() > 0) { for(Long key:workUnit.getKeyList()) { indexer.deleteDocument(key, this); } } } } } private DirectoryReader getReader() throws IOException { DirectoryReader newReader = DirectoryReader.openIfChanged(reader); if(newReader != null) { reader = newReader; } return reader; } @Override public IndexWriter getAndLockWriter() throws IOException { return permanentIndexWriter.getAndLock(); } @Override public void releaseWriter(IndexWriter writer) { permanentIndexWriter.release(writer); } @Override public void deleteDocument(String resourceUrl) { IndexWriter writer = null; try { Term uuidTerm = new Term(AbstractOlatDocument.RESOURCEURL_FIELD_NAME, resourceUrl); writer = permanentIndexWriter.getAndLock(); writer.deleteDocuments(uuidTerm); } catch (IOException e) { log.error("", e); } finally { permanentIndexWriter.release(writer); } } /** * Add or update a lucene document in the permanent index. * @param uuid * @param document */ @Override public void addDocuments(List<Document> documents) { if(documents == null || documents.isEmpty()) return;//nothing to do IndexWriter writer = null; try { DirectoryReader currentReader = getReader(); IndexSearcher searcher = new IndexSearcher(currentReader); writer = permanentIndexWriter.getAndLock(); for(Document document:documents) { if(document != null) { String resourceUrl = document.get(AbstractOlatDocument.RESOURCEURL_FIELD_NAME); Term uuidTerm = new Term(AbstractOlatDocument.RESOURCEURL_FIELD_NAME, resourceUrl); TopDocs hits = searcher.search(new TermQuery(uuidTerm), 10); if(hits.totalHits > 0) { writer.updateDocument(uuidTerm, document); } else { writer.addDocument(document); } } } } catch (IOException e) { log.error("", e); } finally { permanentIndexWriter.release(writer); } } @Override public void addDocument(Document document, IndexWriter writer) { try { String resourceUrl = document.get(AbstractOlatDocument.RESOURCEURL_FIELD_NAME); Term uuidTerm = new Term(AbstractOlatDocument.RESOURCEURL_FIELD_NAME, resourceUrl); DirectoryReader currentReader = getReader(); IndexSearcher searcher = new IndexSearcher(currentReader); TopDocs hits = searcher.search(new TermQuery(uuidTerm), 10); if(hits.totalHits > 0) { writer.updateDocument(uuidTerm, document); } else { writer.addDocument(document); } } catch (IOException e) { log.error("", e); } } }