Merge pull request #123 from snoopdave/solr-impl

Isolate Lucene API in search.lucene
diff --git a/app/pom.xml b/app/pom.xml
index 8c89d28..ae1a622 100644
--- a/app/pom.xml
+++ b/app/pom.xml
@@ -308,6 +308,23 @@
             <version>${lucene.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.solr</groupId>
+            <artifactId>solr-solrj</artifactId>
+            <version>9.1.1</version>
+            <!-- exclude vulnerable dependencies -->
+            <exclusions>
+                <exclusion>
+                    <groupId>io.netty</groupId>
+                    <artifactId>netty-codec</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>io.netty</groupId>
+                    <artifactId>netty-common</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
         <!-- slf4j implementing the apache commons-logging interfaces -->
         <!-- note: commons-logging needs to be excluded in all dependencies transitive depending on it.
         See 2006 RFE https://issues.apache.org/jira/browse/MNG-1977 for maven's missing feature of global exclusions -->
@@ -571,6 +588,20 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.2.4</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.instancio</groupId>
+            <artifactId>instancio-junit</artifactId>
+            <version>2.9.0</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAWebloggerModule.java b/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAWebloggerModule.java
index 8a8b301..d9e1894 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAWebloggerModule.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/jpa/JPAWebloggerModule.java
@@ -48,7 +48,7 @@
 import org.apache.roller.weblogger.business.plugins.PluginManagerImpl;
 import org.apache.roller.weblogger.business.runnable.ThreadManager;
 import org.apache.roller.weblogger.business.search.IndexManager;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
+import org.apache.roller.weblogger.business.search.lucene.LuceneIndexManager;
 import org.apache.roller.weblogger.business.themes.ThemeManager;
 import org.apache.roller.weblogger.business.themes.ThemeManagerImpl;
 import org.apache.roller.weblogger.planet.business.WebloggerRomeFeedFetcher;
@@ -81,7 +81,7 @@
                 
         binder.bind(MediaFileManager.class).to(    JPAMediaFileManagerImpl.class);
         binder.bind(FileContentManager.class).to(  FileContentManagerImpl.class);
-        binder.bind(IndexManager.class).to(        IndexManagerImpl.class);
+        binder.bind(IndexManager.class).to(        LuceneIndexManager.class);
         binder.bind(PluginManager.class).to(       PluginManagerImpl.class);    
         binder.bind(ThemeManager.class).to(        ThemeManagerImpl.class);
         
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/IndexManager.java b/app/src/main/java/org/apache/roller/weblogger/business/search/IndexManager.java
index 85956f0..8a8a933 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/IndexManager.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/IndexManager.java
@@ -19,53 +19,58 @@
 
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.InitializationException;
-import org.apache.roller.weblogger.business.search.operations.IndexOperation;
-import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.business.URLStrategy;
 import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
 
 /**
- * Interface to Roller's Lucene-based search facility.
+ * Interface to Roller's full-text search facility.
  * @author Dave Johnson
  */
-public interface IndexManager
-{
-    /** Does index need to be rebuild */
-    boolean isInconsistentAtStartup();
-    
-    /** Remove user from index, returns immediately and operates in background */
-    void removeWebsiteIndex(Weblog website) throws WebloggerException;
-    
-    /** Remove entry from index, returns immediately and operates in background */
-    void removeEntryIndexOperation(WeblogEntry entry) throws WebloggerException;
-    
-    /** Add entry to index, returns immediately and operates in background */
-    void addEntryIndexOperation(WeblogEntry entry) throws WebloggerException;
-    
-    /** R-index entry, returns immediately and operates in background */
-    void addEntryReIndexOperation(WeblogEntry entry) throws WebloggerException;
-    
-    /** Execute operation immediately */
-    void executeIndexOperationNow(final IndexOperation op);
+public interface IndexManager {
+
+    /**
+     * Initialize the search system.
+     * @throws InitializationException If there is a problem during initialization.
+     */
+    void initialize() throws InitializationException;
+
+    /** Shutdown to be called on application shutdown */
+    void shutdown();
 
     /**
      * Release all resources associated with Roller session.
      */
     void release();
-    
-    
-    /**
-     * Initialize the search system.
-     *
-     * @throws InitializationException If there is a problem during initialization.
-     */
-    void initialize() throws InitializationException;
-    
-    
-    /** Shutdown to be called on application shutdown */
-    void shutdown();
 
-    void rebuildWebsiteIndex(Weblog website) throws WebloggerException;
+    /** Does index need to be rebuilt */
+    boolean isInconsistentAtStartup();
 
-    void rebuildWebsiteIndex() throws WebloggerException;
+    /** Add entry to index, returns immediately and operates in background */
+    void addEntryIndexOperation(WeblogEntry entry) throws WebloggerException;
+    
+    /** Re-index entry, returns immediately and operates in background */
+    void addEntryReIndexOperation(WeblogEntry entry) throws WebloggerException;
 
+    void rebuildWeblogIndex(Weblog weblog) throws WebloggerException;
+
+    void rebuildWeblogIndex() throws WebloggerException;
+
+    /** Remove weblog from index, returns immediately and operates in background */
+    void removeWeblogIndex(Weblog weblog) throws WebloggerException;
+
+    /** Remove entry from index, returns immediately and operates in background */
+    void removeEntryIndexOperation(WeblogEntry entry) throws WebloggerException;
+
+    SearchResultList search(
+        String term,
+        String weblogHandle,
+        String category,
+        String locale,
+        int pageNum,
+        int entryCount,
+        URLStrategy urlStrategy
+    ) throws WebloggerException;
 }
+
+
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/SearchResultList.java b/app/src/main/java/org/apache/roller/weblogger/business/search/SearchResultList.java
new file mode 100644
index 0000000..5fcaab2
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/SearchResultList.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  The ASF licenses this file to You
+ * 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.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+
+/* Created on March 8, 2023 */
+
+package org.apache.roller.weblogger.business.search;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.roller.weblogger.pojos.wrapper.WeblogEntryWrapper;
+
+public class SearchResultList {
+    int limit;
+    int offset;
+    Set<String> categories;
+    List<WeblogEntryWrapper> results;
+    public SearchResultList(
+        List<WeblogEntryWrapper> results, Set<String> categories, int limit, int offset) {
+        this.results = results;
+        this.categories = categories;
+        this.limit = limit;
+        this.offset = offset;
+    }
+    public int getLimit() {
+        return limit;
+    }
+    public int getOffset() {
+        return offset;
+    }
+    public List<WeblogEntryWrapper> getResults() {
+        return results;
+    }
+    public Set<String> getCategories() {
+        return categories;
+    }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/SearchResultMap.java b/app/src/main/java/org/apache/roller/weblogger/business/search/SearchResultMap.java
new file mode 100644
index 0000000..f3c985c
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/SearchResultMap.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  The ASF licenses this file to You
+ * 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.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+
+/* Created on March 8, 2023 */
+
+package org.apache.roller.weblogger.business.search;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import org.apache.roller.weblogger.pojos.wrapper.WeblogEntryWrapper;
+
+public class SearchResultMap {
+    int limit;
+    int offset;
+    Set<String> categories;
+    Map<Date, Set<WeblogEntryWrapper>> results;
+    public SearchResultMap(Map<Date, Set<WeblogEntryWrapper>> results, Set<String> categories, int limit, int offset) {
+        this.results = results;
+        this.categories = categories;
+        this.limit = limit;
+        this.offset = offset;
+    }
+    public int getLimit() {
+        return limit;
+    }
+    public int getOffset() {
+        return offset;
+    }
+    public Map<Date, Set<WeblogEntryWrapper>> getResults() {
+        return results;
+    }
+    public Set<String> getCategories() {
+        return categories;
+    }
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/AddEntryOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/AddEntryOperation.java
similarity index 88%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/AddEntryOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/AddEntryOperation.java
index 1321ada..1d8e500 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/AddEntryOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/AddEntryOperation.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 16, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 
@@ -24,7 +24,6 @@
 import org.apache.commons.logging.LogFactory;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.roller.weblogger.WebloggerException;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
 import org.apache.roller.weblogger.business.Weblogger;
 import org.apache.roller.weblogger.business.WeblogEntryManager;
 import org.apache.roller.weblogger.pojos.WeblogEntry;
@@ -37,7 +36,7 @@
     
     //~ Static fields/initializers =============================================
     
-    private static Log mLogger =
+    private static Log logger =
             LogFactory.getFactory().getInstance(AddEntryOperation.class);
     
     //~ Instance fields ========================================================
@@ -50,7 +49,7 @@
     /**
      * Adds a web log entry into the index.
      */
-    public AddEntryOperation(Weblogger roller, IndexManagerImpl mgr,WeblogEntry data) {
+    public AddEntryOperation(Weblogger roller, LuceneIndexManager mgr, WeblogEntry data) {
         super(mgr);
         this.roller = roller;
         this.data = data;
@@ -69,7 +68,7 @@
             WeblogEntryManager wMgr = roller.getWeblogEntryManager();
             this.data = wMgr.getWeblogEntry(this.data.getId());
         } catch (WebloggerException ex) {
-            mLogger.error("Error getting weblogentry object", ex);
+            logger.error("Error getting weblogentry object", ex);
             return;
         }
         
@@ -78,7 +77,7 @@
                 writer.addDocument(getDocument(data));
             }
         } catch (IOException e) {
-            mLogger.error("Problems adding doc to index", e);
+            logger.error("Problems adding doc to index", e);
         } finally {
             if (roller != null) {
                 roller.release();
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/FieldConstants.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/FieldConstants.java
similarity index 96%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/FieldConstants.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/FieldConstants.java
index 7b19361..bfbd9a2 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/FieldConstants.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/FieldConstants.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 19, 2003 */
-package org.apache.roller.weblogger.business.search;
+package org.apache.roller.weblogger.business.search.lucene;
 
 
 /**
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/IndexOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/IndexOperation.java
similarity index 92%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/IndexOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/IndexOperation.java
index 22d8d7c..7c251e8 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/IndexOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/IndexOperation.java
@@ -15,8 +15,10 @@
  * copyright in this work, please see the NOTICE file in the top level
  * directory of this distribution.
  */
+
 /* Created on Jul 16, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 import java.util.List;
@@ -32,8 +34,6 @@
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.util.BytesRef;
-import org.apache.roller.weblogger.business.search.FieldConstants;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
 import org.apache.roller.weblogger.config.WebloggerConfig;
 import org.apache.roller.weblogger.pojos.WeblogCategory;
 import org.apache.roller.weblogger.pojos.WeblogEntry;
@@ -50,17 +50,17 @@
  */
 public abstract class IndexOperation implements Runnable {
 
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             IndexOperation.class);
 
     // ~ Instance fields
     // ========================================================
-    protected IndexManagerImpl manager;
+    protected LuceneIndexManager manager;
     private IndexWriter writer;
 
     // ~ Constructors
     // ===========================================================
-    public IndexOperation(IndexManagerImpl manager) {
+    public IndexOperation(LuceneIndexManager manager) {
         this.manager = manager;
     }
 
@@ -172,7 +172,7 @@
         try {
 
             LimitTokenCountAnalyzer analyzer = new LimitTokenCountAnalyzer(
-                    IndexManagerImpl.getAnalyzer(),
+                    LuceneIndexManager.getAnalyzer(),
                     WebloggerConfig.getIntProperty("lucene.analyzer.maxTokenCount"));
 
             IndexWriterConfig config = new IndexWriterConfig(analyzer);
@@ -180,7 +180,7 @@
             writer = new IndexWriter(manager.getIndexDirectory(), config);
 
         } catch (IOException e) {
-            mLogger.error("ERROR creating writer", e);
+            logger.error("ERROR creating writer", e);
         }
 
         return writer;
@@ -194,7 +194,7 @@
             try {
                 writer.close();
             } catch (IOException e) {
-                mLogger.error("ERROR closing writer", e);
+                logger.error("ERROR closing writer", e);
             }
         }
     }
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/IndexUtil.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/IndexUtil.java
similarity index 94%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/IndexUtil.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/IndexUtil.java
index 70e25da..1f3f00d 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/IndexUtil.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/IndexUtil.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 20, 2003 */
-package org.apache.roller.weblogger.business.search;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 import java.io.StringReader;
@@ -49,7 +49,7 @@
         if (input == null || field == null) {
             return null;
         }
-        Analyzer analyzer = IndexManagerImpl.getAnalyzer();
+        Analyzer analyzer = LuceneIndexManager.getAnalyzer();
         Term term = null;
         try {
             TokenStream tokens = analyzer.tokenStream(field, new StringReader(input));
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/IndexManagerImpl.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/LuceneIndexManager.java
similarity index 60%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/IndexManagerImpl.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/LuceneIndexManager.java
index 077382b..7a7e5b5 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/IndexManagerImpl.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/LuceneIndexManager.java
@@ -16,41 +16,49 @@
  * directory of this distribution.
  */
 
-package org.apache.roller.weblogger.business.search;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.File;
 import java.io.IOException;
-
 import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
-
 import org.apache.commons.beanutils.ConstructorUtils;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.apache.lucene.analysis.Analyzer;
 import org.apache.lucene.analysis.miscellaneous.LimitTokenCountAnalyzer;
 import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.Document;
 import org.apache.lucene.index.DirectoryReader;
 import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TopFieldDocs;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.store.FSDirectory;
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.InitializationException;
+import org.apache.roller.weblogger.business.URLStrategy;
+import org.apache.roller.weblogger.business.WeblogEntryManager;
 import org.apache.roller.weblogger.business.Weblogger;
-import org.apache.roller.weblogger.business.search.operations.AddEntryOperation;
-import org.apache.roller.weblogger.business.search.operations.IndexOperation;
-import org.apache.roller.weblogger.business.search.operations.ReIndexEntryOperation;
-import org.apache.roller.weblogger.business.search.operations.RebuildWebsiteIndexOperation;
-import org.apache.roller.weblogger.business.search.operations.RemoveEntryOperation;
-import org.apache.roller.weblogger.business.search.operations.RemoveWebsiteIndexOperation;
-import org.apache.roller.weblogger.pojos.WeblogEntry;
-import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.business.WebloggerFactory;
+import org.apache.roller.weblogger.business.search.IndexManager;
+import org.apache.roller.weblogger.business.search.SearchResultList;
 import org.apache.roller.weblogger.config.WebloggerConfig;
+import org.apache.roller.weblogger.config.WebloggerRuntimeConfig;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.pojos.wrapper.WeblogEntryWrapper;
 
 /**
  * Lucene implementation of IndexManager. This is the central entry point into
@@ -60,12 +68,12 @@
  * @author mraible (formatting and making indexDir configurable)
  */
 @com.google.inject.Singleton
-public class IndexManagerImpl implements IndexManager {
+public class LuceneIndexManager implements IndexManager {
 
     private IndexReader reader;
     private final Weblogger roller;
 
-    private final static Log mLogger = LogFactory.getFactory().getInstance(IndexManagerImpl.class);
+    private final static Log logger = LogFactory.getFactory().getInstance(LuceneIndexManager.class);
 
     private boolean searchEnabled = true;
 
@@ -87,7 +95,7 @@
      * @param roller - the weblogger instance
      */
     @com.google.inject.Inject
-    protected IndexManagerImpl(Weblogger roller) {
+    protected LuceneIndexManager(Weblogger roller) {
         this.roller = roller;
 
         // check config to see if the internal search is enabled
@@ -102,8 +110,8 @@
         this.indexDir = searchIndexDir.replace('/', File.separatorChar);
 
         // a little debugging
-        mLogger.info("search enabled: " + this.searchEnabled);
-        mLogger.info("index dir: " + this.indexDir);
+        logger.info("search enabled: " + this.searchEnabled);
+        logger.info("index dir: " + this.indexDir);
 
         String test = indexDir + File.separator + ".index-inconsistent";
         indexConsistencyMarker = new File(test);
@@ -120,7 +128,7 @@
 
             // delete index if inconsistency marker exists
             if (indexConsistencyMarker.exists()) {
-                mLogger.debug("Index inconsistent: marker exists");
+                logger.debug("Index inconsistent: marker exists");
                 inconsistentAtStartup = true;
                 deleteIndex();
             } else {
@@ -129,11 +137,11 @@
                     if (!makeIndexDir.exists()) {
                         makeIndexDir.mkdirs();
                         inconsistentAtStartup = true;
-                        mLogger.debug("Index inconsistent: new");
+                        logger.debug("Index inconsistent: new");
                     }
                     indexConsistencyMarker.createNewFile();
                 } catch (IOException e) {
-                    mLogger.error(e);
+                    logger.error(e);
                 }
             }
 
@@ -145,43 +153,43 @@
                         reader = DirectoryReader.open(getIndexDirectory());
                     }
                 } catch (IOException | IllegalArgumentException ex) {  // IAE for incompatible codecs
-                    mLogger.warn("Failed to open search index, scheduling rebuild.", ex);
+                    logger.warn("Failed to open search index, scheduling rebuild.", ex);
                     inconsistentAtStartup = true;
                     deleteIndex();
                 }
             } else {
-                mLogger.debug("Creating index");
+                logger.debug("Creating index");
                 inconsistentAtStartup = true;
                 deleteIndex();
                 createIndex(getIndexDirectory());
             }
 
             if (inconsistentAtStartup) {
-                mLogger.info("Index was inconsistent. Rebuilding index in the background...");
+                logger.info("Index was inconsistent. Rebuilding index in the background...");
                 try {
-                    rebuildWebsiteIndex();
+                    rebuildWeblogIndex();
                 } catch (WebloggerException ex) {
-                    mLogger.error("ERROR: scheduling re-index operation", ex);
+                    logger.error("ERROR: scheduling re-index operation", ex);
                 }
             } else {
-                mLogger.info("Index initialized and ready for use.");
+                logger.info("Index initialized and ready for use.");
             }
         }
 
     }
 
     @Override
-    public void rebuildWebsiteIndex() throws WebloggerException {
+    public void rebuildWeblogIndex() throws WebloggerException {
         scheduleIndexOperation(new RebuildWebsiteIndexOperation(roller, this, null));
     }
 
     @Override
-    public void rebuildWebsiteIndex(Weblog website) throws WebloggerException {
+    public void rebuildWeblogIndex(Weblog website) throws WebloggerException {
         scheduleIndexOperation(new RebuildWebsiteIndexOperation(roller, this, website));
     }
 
     @Override
-    public void removeWebsiteIndex(Weblog website) throws WebloggerException {
+    public void removeWeblogIndex(Weblog website) throws WebloggerException {
         scheduleIndexOperation(new RemoveWebsiteIndexOperation(roller, this, website));
     }
 
@@ -200,6 +208,45 @@
         executeIndexOperationNow(new RemoveEntryOperation(roller, this, entry));
     }
 
+    @Override
+    public SearchResultList search(
+        String term,
+        String weblogHandle,
+        String category,
+        String locale,
+        int pageNum,
+        int entryCount,
+        URLStrategy urlStrategy) throws WebloggerException {
+
+        SearchOperation search = new SearchOperation(this);
+        search.setTerm(term);
+        boolean weblogSpecific = !WebloggerRuntimeConfig.isSiteWideWeblog(weblogHandle);
+        if (weblogSpecific) {
+            search.setWeblogHandle(weblogHandle);
+        }
+        if (category != null) {
+            search.setCategory(category);
+        }
+        if (locale != null) {
+            search.setLocale(locale);
+        }
+
+        executeIndexOperationNow(search);
+        if (search.getResultsCount() >= 0) {
+            TopFieldDocs docs = search.getResults();
+            ScoreDoc[] hitsArr = docs.scoreDocs;
+            return convertHitsToEntryList(
+                hitsArr,
+                search,
+                pageNum,
+                entryCount,
+                weblogHandle,
+                weblogSpecific,
+                urlStrategy);
+        }
+        throw new WebloggerException("Error executing search");
+    }
+
     public ReadWriteLock getReadWriteLock() {
         return rwl;
     }
@@ -224,10 +271,10 @@
             final Class<?> clazz = Class.forName(className);
             return (Analyzer) ConstructorUtils.invokeConstructor(clazz, null);
         } catch (final ClassNotFoundException e) {
-            mLogger.error("failed to lookup analyzer class: " + className, e);
+            logger.error("failed to lookup analyzer class: " + className, e);
             return instantiateDefaultAnalyzer();
         } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
-            mLogger.error("failed to instantiate analyzer: " + className, e);
+            logger.error("failed to instantiate analyzer: " + className, e);
             return instantiateDefaultAnalyzer();
         }
     }
@@ -240,29 +287,27 @@
         try {
             // only if search is enabled
             if (this.searchEnabled) {
-                mLogger.debug("Starting scheduled index operation: "
+                logger.debug("Starting scheduled index operation: "
                         + op.getClass().getName());
                 roller.getThreadManager().executeInBackground(op);
             }
         } catch (InterruptedException e) {
-            mLogger.error("Error executing operation", e);
+            logger.error("Error executing operation", e);
         }
     }
 
     /**
      * @param op
      */
-    @Override
-    public void executeIndexOperationNow(final IndexOperation op) {
+    private void executeIndexOperationNow(final IndexOperation op) {
         try {
             // only if search is enabled
             if (this.searchEnabled) {
-                mLogger.debug("Executing index operation now: "
-                        + op.getClass().getName());
+                logger.debug("Executing index operation now: " + op.getClass().getName());
                 roller.getThreadManager().executeInForeground(op);
             }
         } catch (InterruptedException e) {
-            mLogger.error("Error executing operation", e);
+            logger.error("Error executing operation", e);
         }
     }
 
@@ -275,7 +320,7 @@
             try {
                 reader = DirectoryReader.open(getIndexDirectory());
             } catch (IOException ex) {
-                mLogger.error("Error opening DirectoryReader", ex);
+                logger.error("Error opening DirectoryReader", ex);
                 throw new RuntimeException(ex);
             }
         }
@@ -293,7 +338,7 @@
         try {
             return FSDirectory.open(Path.of(indexDir));
         } catch (IOException e) {
-            mLogger.error("Problem accessing index directory", e);
+            logger.error("Problem accessing index directory", e);
         }
         return null;
     }
@@ -302,7 +347,7 @@
         try {
             return DirectoryReader.indexExists(getIndexDirectory());
         } catch (IOException e) {
-            mLogger.error("Problem accessing index directory", e);
+            logger.error("Problem accessing index directory", e);
         }
         return false;
     }
@@ -317,7 +362,7 @@
                 Files.delete(Path.of(indexDir, file));
             }
         } catch (IOException ex) {
-             mLogger.error("Problem accessing index directory", ex);
+             logger.error("Problem accessing index directory", ex);
         }
 
     }
@@ -329,18 +374,18 @@
 
             IndexWriterConfig config = new IndexWriterConfig(
                     new LimitTokenCountAnalyzer(
-                            IndexManagerImpl.getAnalyzer(), 128));
+                            LuceneIndexManager.getAnalyzer(), 128));
 
             writer = new IndexWriter(dir, config);
 
         } catch (IOException e) {
-            mLogger.error("Error creating index", e);
+            logger.error("Error creating index", e);
         } finally {
             if (writer != null) {
                 try {
                     writer.close();
                 } catch (IOException ex) {
-                    mLogger.warn("Unable to close IndexWriter.", ex);
+                    logger.warn("Unable to close IndexWriter.", ex);
                 }
             }
         }
@@ -360,9 +405,81 @@
             try {
                 reader.close();
             } catch (IOException ex) {
-                mLogger.error("Unable to close reader.", ex);
+                logger.error("Unable to close reader.", ex);
             }
         }
     }
 
+    /**
+     * Convert hits to entries.
+     *
+     * @param hits
+     *            the hits
+     * @param search
+     *            the search
+     * @throws WebloggerException
+     *             the weblogger exception
+     */
+    static SearchResultList convertHitsToEntryList(
+        ScoreDoc[] hits,
+        SearchOperation search,
+        int pageNum,
+        int entryCount,
+        String weblogHandle,
+        boolean websiteSpecificSearch,
+        URLStrategy urlStrategy)
+        throws WebloggerException {
+
+        List<WeblogEntryWrapper> results = new ArrayList<>();
+
+        // determine offset
+        int offset = pageNum * entryCount;
+        if (offset >= hits.length) {
+            offset = 0;
+        }
+
+        // determine limit
+        int limit = entryCount;
+        if (offset + limit > hits.length) {
+            limit = hits.length - offset;
+        }
+
+        try {
+            Set<String> categories = new TreeSet<>();
+            TreeSet<String> categorySet = new TreeSet<>();
+            Weblogger roller = WebloggerFactory.getWeblogger();
+            WeblogEntryManager weblogMgr = roller.getWeblogEntryManager();
+
+            WeblogEntry entry;
+            Document doc;
+            String handle;
+            Timestamp now = new Timestamp(new Date().getTime());
+            for (int i = offset; i < offset + limit; i++) {
+                doc = search.getSearcher().doc(hits[i].doc);
+                handle = doc.getField(FieldConstants.WEBSITE_HANDLE).stringValue();
+                entry = weblogMgr.getWeblogEntry(doc.getField(FieldConstants.ID).stringValue());
+
+                if (!(websiteSpecificSearch && handle.equals(weblogHandle))
+                    && doc.getField(FieldConstants.CATEGORY) != null) {
+                    categorySet.add(doc.getField(FieldConstants.CATEGORY).stringValue());
+                }
+
+                // maybe null if search result returned inactive user
+                // or entry's user is not the requested user.
+                // but don't return future posts
+                if (entry != null && entry.getPubTime().before(now)) {
+                    results.add(WeblogEntryWrapper.wrap(entry, urlStrategy));
+                }
+            }
+
+            if (!categorySet.isEmpty()) {
+                categories = categorySet;
+            }
+
+            return new SearchResultList(results, categories, limit, offset);
+
+        } catch (IOException e) {
+            throw new WebloggerException(e);
+        }
+    }
 }
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/ReIndexEntryOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/ReIndexEntryOperation.java
similarity index 86%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/ReIndexEntryOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/ReIndexEntryOperation.java
index 224908b..e0d8e3c 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/ReIndexEntryOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/ReIndexEntryOperation.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 16, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 
@@ -27,8 +27,6 @@
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.WeblogEntryManager;
 import org.apache.roller.weblogger.business.Weblogger;
-import org.apache.roller.weblogger.business.search.FieldConstants;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
 import org.apache.roller.weblogger.pojos.WeblogEntry;
 
 /**
@@ -41,7 +39,7 @@
     // ~ Static fields/initializers
     // =============================================
 
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             AddEntryOperation.class);
 
     // ~ Instance fields
@@ -56,7 +54,7 @@
     /**
      * Adds a web log entry into the index.
      */
-    public ReIndexEntryOperation(Weblogger roller, IndexManagerImpl mgr,
+    public ReIndexEntryOperation(Weblogger roller, LuceneIndexManager mgr,
             WeblogEntry data) {
         super(mgr);
         this.roller = roller;
@@ -76,7 +74,7 @@
             WeblogEntryManager wMgr = roller.getWeblogEntryManager();
             this.data = wMgr.getWeblogEntry(this.data.getId());
         } catch (WebloggerException ex) {
-            mLogger.error("Error getting weblogentry object", ex);
+            logger.error("Error getting weblogentry object", ex);
             return;
         }
 
@@ -92,7 +90,7 @@
                 writer.addDocument(getDocument(data));
             }
         } catch (IOException e) {
-            mLogger.error("Problems adding/deleting doc to index", e);
+            logger.error("Problems adding/deleting doc to index", e);
         } finally {
             if (roller != null) {
                 roller.release();
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/ReadFromIndexOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/ReadFromIndexOperation.java
similarity index 80%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/ReadFromIndexOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/ReadFromIndexOperation.java
index e45ac3b..81e4bb9 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/ReadFromIndexOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/ReadFromIndexOperation.java
@@ -15,21 +15,20 @@
  * copyright in this work, please see the NOTICE file in the top level
  * directory of this distribution.
  */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
 
 /**
  * @author aim4min
  */
 public abstract class ReadFromIndexOperation extends IndexOperation {
-    public ReadFromIndexOperation(IndexManagerImpl mgr) {
+    public ReadFromIndexOperation(LuceneIndexManager mgr) {
         super(mgr);
     }
     
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             ReadFromIndexOperation.class);
     
     @Override
@@ -39,7 +38,7 @@
             doRun();
 
         } catch (Exception e) {
-            mLogger.error("Error acquiring read lock on index", e);
+            logger.error("Error acquiring read lock on index", e);
         } finally {
             manager.getReadWriteLock().readLock().unlock();
         }
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/RebuildWebsiteIndexOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RebuildWebsiteIndexOperation.java
similarity index 84%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/RebuildWebsiteIndexOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RebuildWebsiteIndexOperation.java
index 7a43ba5..69b8217 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/RebuildWebsiteIndexOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RebuildWebsiteIndexOperation.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 16, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.text.MessageFormat;
 import java.util.Date;
@@ -30,9 +30,6 @@
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.WeblogEntryManager;
 import org.apache.roller.weblogger.business.Weblogger;
-import org.apache.roller.weblogger.business.search.FieldConstants;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
-import org.apache.roller.weblogger.business.search.IndexUtil;
 import org.apache.roller.weblogger.pojos.Weblog;
 import org.apache.roller.weblogger.pojos.WeblogEntry;
 import org.apache.roller.weblogger.pojos.WeblogEntry.PubStatus;
@@ -48,7 +45,7 @@
     // ~ Static fields/initializers
     // =============================================
 
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             RebuildWebsiteIndexOperation.class);
 
     // ~ Instance fields
@@ -66,7 +63,7 @@
      * @param website
      *            The website to rebuild the index for, or null for all users.
      */
-    public RebuildWebsiteIndexOperation(Weblogger roller, IndexManagerImpl mgr,
+    public RebuildWebsiteIndexOperation(Weblogger roller, LuceneIndexManager mgr,
             Weblog website) {
         super(mgr);
         this.roller = roller;
@@ -85,16 +82,16 @@
         // the weblog object passed in as a detached object which is proned to
         // lazy initialization problems, so requery for the object now
         if (this.website != null) {
-            mLogger.debug("Reindexining weblog " + website.getHandle());
+            logger.debug("Reindexining weblog " + website.getHandle());
             try {
                 this.website = roller.getWeblogManager().getWeblog(
                         this.website.getId());
             } catch (WebloggerException ex) {
-                mLogger.error("Error getting website object", ex);
+                logger.error("Error getting website object", ex);
                 return;
             }
         } else {
-            mLogger.debug("Reindexining entire site");
+            logger.debug("Reindexining entire site");
         }
 
         IndexWriter writer = beginWriting();
@@ -124,11 +121,11 @@
                 wesc.setStatus(PubStatus.PUBLISHED);
                 List<WeblogEntry> entries = weblogManager.getWeblogEntries(wesc);
 
-                mLogger.debug("Entries to index: " + entries.size());
+                logger.debug("Entries to index: " + entries.size());
 
                 for (WeblogEntry entry : entries) {
                     writer.addDocument(getDocument(entry));
-                    mLogger.debug(MessageFormat.format(
+                    logger.debug(MessageFormat.format(
                             "Indexed entry {0}: {1}",
                             entry.getPubTime(), entry.getAnchor()));
                 }
@@ -137,7 +134,7 @@
                 roller.release();
             }
         } catch (Exception e) {
-            mLogger.error("ERROR adding/deleting doc to index", e);
+            logger.error("ERROR adding/deleting doc to index", e);
         } finally {
             endWriting();
             if (roller != null) {
@@ -149,10 +146,10 @@
         double length = (end.getTime() - start.getTime()) / (double) RollerConstants.SEC_IN_MS;
 
         if (website == null) {
-            mLogger.info("Completed rebuilding index for all users in '"
+            logger.info("Completed rebuilding index for all users in '"
                     + length + "' secs");
         } else {
-            mLogger.info("Completed rebuilding index for website handle: '"
+            logger.info("Completed rebuilding index for website handle: '"
                     + website.getHandle() + "' in '" + length + "' seconds");
         }
     }
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/RemoveEntryOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RemoveEntryOperation.java
similarity index 85%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/RemoveEntryOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RemoveEntryOperation.java
index aae130d..1280a02 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/RemoveEntryOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RemoveEntryOperation.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 16, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 
@@ -27,8 +27,6 @@
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.WeblogEntryManager;
 import org.apache.roller.weblogger.business.Weblogger;
-import org.apache.roller.weblogger.business.search.FieldConstants;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
 import org.apache.roller.weblogger.pojos.WeblogEntry;
 
 /**
@@ -41,7 +39,7 @@
     // ~ Static fields/initializers
     // =============================================
 
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             RemoveEntryOperation.class);
 
     // ~ Instance fields
@@ -53,7 +51,7 @@
     // ~ Constructors
     // ===========================================================
 
-    public RemoveEntryOperation(Weblogger roller, IndexManagerImpl mgr,
+    public RemoveEntryOperation(Weblogger roller, LuceneIndexManager mgr,
             WeblogEntry data) {
         super(mgr);
         this.roller = roller;
@@ -73,7 +71,7 @@
             WeblogEntryManager wMgr = roller.getWeblogEntryManager();
             this.data = wMgr.getWeblogEntry(this.data.getId());
         } catch (WebloggerException ex) {
-            mLogger.error("Error getting weblogentry object", ex);
+            logger.error("Error getting weblogentry object", ex);
             return;
         }
 
@@ -84,7 +82,7 @@
                 writer.deleteDocuments(term);
             }
         } catch (IOException e) {
-            mLogger.error("Error deleting doc from index", e);
+            logger.error("Error deleting doc from index", e);
         } finally {
             endWriting();
         }
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/RemoveWebsiteIndexOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RemoveWebsiteIndexOperation.java
similarity index 85%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/RemoveWebsiteIndexOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RemoveWebsiteIndexOperation.java
index 6c5d154..8c7a401 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/RemoveWebsiteIndexOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/RemoveWebsiteIndexOperation.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 16, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 import java.util.Date;
@@ -28,9 +28,6 @@
 import org.apache.roller.util.RollerConstants;
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.Weblogger;
-import org.apache.roller.weblogger.business.search.FieldConstants;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
-import org.apache.roller.weblogger.business.search.IndexUtil;
 import org.apache.roller.weblogger.pojos.Weblog;
 
 /**
@@ -43,7 +40,7 @@
     // ~ Static fields/initializers
     // =============================================
 
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             RemoveWebsiteIndexOperation.class);
 
     // ~ Instance fields
@@ -61,7 +58,7 @@
      * @param website
      *            The website to rebuild the index for, or null for all sites.
      */
-    public RemoveWebsiteIndexOperation(Weblogger roller, IndexManagerImpl mgr,
+    public RemoveWebsiteIndexOperation(Weblogger roller, LuceneIndexManager mgr,
             Weblog website) {
         super(mgr);
         this.roller = roller;
@@ -82,7 +79,7 @@
             this.website = roller.getWeblogManager().getWeblog(
                     this.website.getId());
         } catch (WebloggerException ex) {
-            mLogger.error("Error getting website object", ex);
+            logger.error("Error getting website object", ex);
             return;
         }
 
@@ -101,7 +98,7 @@
                 }
             }
         } catch (IOException e) {
-            mLogger.info("Problems deleting doc from index", e);
+            logger.info("Problems deleting doc from index", e);
         } finally {
             endWriting();
         }
@@ -110,7 +107,7 @@
         double length = (end.getTime() - start.getTime()) / (double) RollerConstants.SEC_IN_MS;
 
         if (website != null) {
-            mLogger.info("Completed deleting indices for website '"
+            logger.info("Completed deleting indices for website '"
                     + website.getName() + "' in '" + length + "' seconds");
         }
     }
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/SearchOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/SearchOperation.java
similarity index 81%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/SearchOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/SearchOperation.java
index 165313d..a1f2f4e 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/SearchOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/SearchOperation.java
@@ -16,7 +16,7 @@
  * directory of this distribution.
  */
 /* Created on Jul 18, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import java.io.IOException;
 
@@ -34,10 +34,7 @@
 import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.TopFieldDocs;
-import org.apache.roller.weblogger.business.search.FieldConstants;
 import org.apache.roller.weblogger.business.search.IndexManager;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
-import org.apache.roller.weblogger.business.search.IndexUtil;
 
 /**
  * An operation that searches the index.
@@ -49,12 +46,14 @@
     // ~ Static fields/initializers
     // =============================================
 
-    private static Log mLogger = LogFactory.getFactory().getInstance(
+    private static Log logger = LogFactory.getFactory().getInstance(
             SearchOperation.class);
 
     private static final String[] SEARCH_FIELDS = new String[] {
-            FieldConstants.CONTENT, FieldConstants.TITLE,
-            FieldConstants.C_CONTENT };
+        FieldConstants.CONTENT,
+        FieldConstants.TITLE,
+        FieldConstants.C_CONTENT
+    };
 
     private static final Sort SORTER = new Sort(new SortField(
             FieldConstants.PUBLISHED, SortField.Type.STRING, true));
@@ -66,7 +65,7 @@
     private TopFieldDocs searchresults;
 
     private String term;
-    private String websiteHandle;
+    private String weblogHandle;
     private String category;
     private String locale;
     private String parseError;
@@ -80,7 +79,7 @@
     public SearchOperation(IndexManager mgr) {
         // TODO: finish moving IndexManager to backend, so this cast is not
         // needed
-        super((IndexManagerImpl) mgr);
+        super((LuceneIndexManager) mgr);
     }
 
     // ~ Methods
@@ -106,7 +105,7 @@
             searcher = new IndexSearcher(reader);
 
             MultiFieldQueryParser multiParser = new MultiFieldQueryParser(
-                    SEARCH_FIELDS, IndexManagerImpl.getAnalyzer());
+                    SEARCH_FIELDS, LuceneIndexManager.getAnalyzer());
 
             // Make it an AND by default. Comment this out for an or (default)
             multiParser.setDefaultOperator(MultiFieldQueryParser.Operator.AND);
@@ -114,37 +113,34 @@
             // Create a query object out of our term
             Query query = multiParser.parse(term);
 
-            Term tUsername = IndexUtil.getTerm(FieldConstants.WEBSITE_HANDLE,
-                    websiteHandle);
-
-            if (tUsername != null) {
+            Term handleTerm = IndexUtil.getTerm(FieldConstants.WEBSITE_HANDLE, weblogHandle);
+            if (handleTerm != null) {
                 query = new BooleanQuery.Builder()
                     .add(query, BooleanClause.Occur.MUST)
-                    .add(new TermQuery(tUsername), BooleanClause.Occur.MUST)
+                    .add(new TermQuery(handleTerm), BooleanClause.Occur.MUST)
                     .build();
             }
 
             if (category != null) {
-                Term tCategory = new Term(FieldConstants.CATEGORY, category.toLowerCase());
+                Term catTerm = new Term(FieldConstants.CATEGORY, category.toLowerCase());
                 query = new BooleanQuery.Builder()
                     .add(query, BooleanClause.Occur.MUST)
-                    .add(new TermQuery(tCategory), BooleanClause.Occur.MUST)
+                    .add(new TermQuery(catTerm), BooleanClause.Occur.MUST)
                     .build();
             }
 
-            Term tLocale = IndexUtil.getTerm(FieldConstants.LOCALE, locale);
-
-            if (tLocale != null) {
+            Term localeTerm = IndexUtil.getTerm(FieldConstants.LOCALE, locale);
+            if (localeTerm != null) {
                 query = new BooleanQuery.Builder()
                     .add(query, BooleanClause.Occur.MUST)
-                    .add(new TermQuery(tLocale), BooleanClause.Occur.MUST)
+                    .add(new TermQuery(localeTerm), BooleanClause.Occur.MUST)
                     .build();
             }
 
             searchresults = searcher.search(query, docLimit, SORTER);
 
         } catch (IOException e) {
-            mLogger.error("Error searching index", e);
+            logger.error("Error searching index", e);
             parseError = e.getMessage();
 
         } catch (ParseException e) {
@@ -206,11 +202,11 @@
     /**
      * Sets the website handle.
      * 
-     * @param websiteHandle
+     * @param weblogHandle
      *            the new website handle
      */
-    public void setWebsiteHandle(String websiteHandle) {
-        this.websiteHandle = websiteHandle;
+    public void setWeblogHandle(String weblogHandle) {
+        this.weblogHandle = weblogHandle;
     }
 
     /**
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/WriteToIndexOperation.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/WriteToIndexOperation.java
similarity index 78%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/WriteToIndexOperation.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/WriteToIndexOperation.java
index b6cef10..17a45c1 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/WriteToIndexOperation.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/WriteToIndexOperation.java
@@ -16,11 +16,10 @@
  * directory of this distribution.
  */
 /* Created on Aug 12, 2003 */
-package org.apache.roller.weblogger.business.search.operations;
+package org.apache.roller.weblogger.business.search.lucene;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
 
 /**
  * An operation that writes to index.
@@ -28,23 +27,23 @@
  */
 public abstract class WriteToIndexOperation extends IndexOperation {
     
-    public WriteToIndexOperation(IndexManagerImpl mgr) {
+    public WriteToIndexOperation(LuceneIndexManager mgr) {
         super(mgr);
     }
     
-    private static Log mLogger =
+    private static Log logger =
             LogFactory.getFactory().getInstance(WriteToIndexOperation.class);
     
     @Override
     public void run() {
         try {
             manager.getReadWriteLock().writeLock().lock();
-            mLogger.debug("Starting search index operation");
+            logger.debug("Starting search index operation");
             doRun();
-            mLogger.debug("Search index operation complete");
+            logger.debug("Search index operation complete");
 
         } catch (Exception e) {
-            mLogger.error("Error acquiring write lock on index", e);
+            logger.error("Error acquiring write lock on index", e);
             
         } finally {
             manager.getReadWriteLock().writeLock().unlock();
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/package-info.java b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/package-info.java
similarity index 89%
rename from app/src/main/java/org/apache/roller/weblogger/business/search/operations/package-info.java
rename to app/src/main/java/org/apache/roller/weblogger/business/search/lucene/package-info.java
index d4390b6..2999916 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/operations/package-info.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/lucene/package-info.java
@@ -17,6 +17,6 @@
  */
 
 /**
- * Lucene-based search operations.
+ * Lucene search index implementation.
  */
-package org.apache.roller.weblogger.business.search.operations;
\ No newline at end of file
+package org.apache.roller.weblogger.business.search.lucene;
\ No newline at end of file
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/package-info.java b/app/src/main/java/org/apache/roller/weblogger/business/search/package-info.java
index 6c735b6..d9c5796 100644
--- a/app/src/main/java/org/apache/roller/weblogger/business/search/package-info.java
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/package-info.java
@@ -17,6 +17,6 @@
  */
 
 /**
- * Search index manager implementation uses Lucene.
+ * Search index interface with Lucene and Solr implementations.
  */
 package org.apache.roller.weblogger.business.search;
\ No newline at end of file
diff --git a/app/src/main/java/org/apache/roller/weblogger/business/search/solr/SolrIndexManager.java b/app/src/main/java/org/apache/roller/weblogger/business/search/solr/SolrIndexManager.java
new file mode 100644
index 0000000..c71d7a9
--- /dev/null
+++ b/app/src/main/java/org/apache/roller/weblogger/business/search/solr/SolrIndexManager.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  The ASF licenses this file to You
+ * 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.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+
+/* Created on March 8, 2023 */
+
+package org.apache.roller.weblogger.business.search.solr;
+
+import org.apache.roller.weblogger.WebloggerException;
+import org.apache.roller.weblogger.business.InitializationException;
+import org.apache.roller.weblogger.business.URLStrategy;
+import org.apache.roller.weblogger.business.search.IndexManager;
+import org.apache.roller.weblogger.business.search.SearchResultList;
+import org.apache.roller.weblogger.business.search.SearchResultMap;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+
+public class SolrIndexManager implements IndexManager {
+    @Override
+    public void initialize() throws InitializationException {
+
+    }
+
+    @Override
+    public void shutdown() {
+
+    }
+
+    @Override
+    public void release() {
+
+    }
+
+    @Override
+    public boolean isInconsistentAtStartup() {
+        return false;
+    }
+
+    @Override
+    public void addEntryIndexOperation(WeblogEntry entry) throws WebloggerException {
+
+    }
+
+    @Override
+    public void addEntryReIndexOperation(WeblogEntry entry) throws WebloggerException {
+
+    }
+
+    @Override
+    public void rebuildWeblogIndex(Weblog weblog) throws WebloggerException {
+
+    }
+
+    @Override
+    public void rebuildWeblogIndex() throws WebloggerException {
+
+    }
+
+    @Override
+    public void removeWeblogIndex(Weblog weblog) throws WebloggerException {
+
+    }
+
+    @Override
+    public void removeEntryIndexOperation(WeblogEntry entry) throws WebloggerException {
+
+    }
+
+    @Override
+    public SearchResultList search(String term, String weblogHandle, String category, String locale, int pageNum, int entryCount, URLStrategy urlStrategy) throws WebloggerException {
+        return null;
+    }
+
+}
diff --git a/app/src/main/java/org/apache/roller/weblogger/pojos/WeblogEntry.java b/app/src/main/java/org/apache/roller/weblogger/pojos/WeblogEntry.java
index 032dfcb..7cf07da 100644
--- a/app/src/main/java/org/apache/roller/weblogger/pojos/WeblogEntry.java
+++ b/app/src/main/java/org/apache/roller/weblogger/pojos/WeblogEntry.java
@@ -530,7 +530,7 @@
     }
 
     @SuppressWarnings("unused")
-    private void setTags(Set<WeblogEntryTag> tagSet) throws WebloggerException {
+    public void setTags(Set<WeblogEntryTag> tagSet) throws WebloggerException {
          this.tagSet = tagSet;
          this.removedTags = new HashSet<>();
          this.addedTags = new HashSet<>();
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModel.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModel.java
index ce6f9be..1e1adf2 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModel.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModel.java
@@ -18,32 +18,19 @@
 
 package org.apache.roller.weblogger.ui.rendering.model;
 
-import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.TreeSet;
 import org.apache.commons.text.StringEscapeUtils;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.TopFieldDocs;
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.URLStrategy;
-import org.apache.roller.weblogger.business.WeblogEntryManager;
-import org.apache.roller.weblogger.business.Weblogger;
 import org.apache.roller.weblogger.business.WebloggerFactory;
-import org.apache.roller.weblogger.business.search.FieldConstants;
 import org.apache.roller.weblogger.business.search.IndexManager;
-import org.apache.roller.weblogger.business.search.operations.SearchOperation;
+import org.apache.roller.weblogger.business.search.SearchResultList;
 import org.apache.roller.weblogger.config.WebloggerRuntimeConfig;
 import org.apache.roller.weblogger.pojos.Weblog;
-import org.apache.roller.weblogger.pojos.WeblogEntry;
 import org.apache.roller.weblogger.pojos.wrapper.WeblogCategoryWrapper;
 import org.apache.roller.weblogger.pojos.wrapper.WeblogEntryWrapper;
 import org.apache.roller.weblogger.pojos.wrapper.WeblogWrapper;
@@ -68,19 +55,17 @@
 	// the pager used by the 3.0+ rendering system
 	private SearchResultsFeedPager pager = null;
 
-	private final List<WeblogEntryWrapper> results = new ArrayList<>();
+	private List<WeblogEntryWrapper> results = new ArrayList<>();
 
 	private Set<String> categories = Collections.emptySet();
 
-	private boolean websiteSpecificSearch = true;
-
 	private int hits = 0;
 	private int offset = 0;
 	private int limit = 0;
 
-	private int entryCount = 0;
+	private String errorMessage = "";
 
-    @Override
+	@Override
 	public String getModelName() {
 		return "model";
 	}
@@ -89,11 +74,9 @@
 	public void init(Map<String, Object> initData) throws WebloggerException {
 
 		// we expect the init data to contain a weblogRequest object
-		WeblogRequest weblogRequest = (WeblogRequest) initData
-				.get("parsedRequest");
+		WeblogRequest weblogRequest = (WeblogRequest) initData.get("parsedRequest");
 		if (weblogRequest == null) {
-			throw new WebloggerException(
-					"expected weblogRequest from init data");
+			throw new WebloggerException("expected weblogRequest from init data");
 		}
 
 		if (weblogRequest instanceof WeblogFeedRequest) {
@@ -101,7 +84,7 @@
 		} else {
 			throw new WebloggerException(
 					"weblogRequest is not a WeblogFeedRequest."
-							+ "  FeedModel only supports feed requests.");
+					+ "  FeedModel only supports feed requests.");
 		}
 
 		// look for url strategy
@@ -126,38 +109,28 @@
 			return;
 		}
 
-		this.entryCount = WebloggerRuntimeConfig
-				.getIntProperty("site.newsfeeds.defaultEntries");
+		int entryCount = WebloggerRuntimeConfig.getIntProperty("site.newsfeeds.defaultEntries");
 
 		// setup the search
-		IndexManager indexMgr = WebloggerFactory.getWeblogger()
-				.getIndexManager();
+		IndexManager indexMgr = WebloggerFactory.getWeblogger().getIndexManager();
+		try {
+			SearchResultList searchResult = indexMgr.search(
+				feedRequest.getTerm(),
+				feedRequest.getWeblogHandle(),
+				feedRequest.getWeblogCategoryName(),
+				feedRequest.getLocale(),
+				feedRequest.getPage(),
+				entryCount,
+				urlStrategy
+			);
+			this.hits = searchResult.getResults().size();
+			this.offset = searchResult.getOffset();
+			this.limit = searchResult.getLimit();
+			this.results = searchResult.getResults();
+			this.categories = searchResult.getCategories();
 
-		SearchOperation search = new SearchOperation(indexMgr);
-		search.setTerm(feedRequest.getTerm());
-
-		if (WebloggerRuntimeConfig.isSiteWideWeblog(feedRequest
-				.getWeblogHandle())) {
-			this.websiteSpecificSearch = false;
-		} else {
-			search.setWebsiteHandle(feedRequest.getWeblogHandle());
-		}
-
-		if (StringUtils.isNotEmpty(feedRequest.getWeblogCategoryName())) {
-			search.setCategory(feedRequest.getWeblogCategoryName());
-		}
-
-		// execute search
-		indexMgr.executeIndexOperationNow(search);
-
-		if (search.getResultsCount() > -1) {
-
-			TopFieldDocs docs = search.getResults();
-			ScoreDoc[] hitsArr = docs.scoreDocs;
-			this.hits = search.getResultsCount();
-
-			// Convert the Hits into WeblogEntryData instances.
-			convertHitsToEntries(hitsArr, search);
+		} catch (WebloggerException we) {
+			errorMessage = we.getMessage();
 		}
 
 		// search completed, setup pager based on results
@@ -170,68 +143,6 @@
 		return pager;
 	}
 
-	/**
-	 * Convert hits to entries.
-	 * 
-	 * @param hits
-	 *            the hits
-	 * @param search
-	 *            the search
-	 * @throws WebloggerException
-	 *             the weblogger exception
-	 */
-	private void convertHitsToEntries(ScoreDoc[] hits, SearchOperation search)
-			throws WebloggerException {
-
-		// determine offset
-		this.offset = feedRequest.getPage() * this.entryCount;
-		if (this.offset >= hits.length) {
-			this.offset = 0;
-		}
-
-		// determine limit
-		this.limit = this.entryCount;
-		if (this.offset + this.limit > hits.length) {
-			this.limit = hits.length - this.offset;
-		}
-
-		try {
-			TreeSet<String> categorySet = new TreeSet<>();
-			Weblogger roller = WebloggerFactory.getWeblogger();
-			WeblogEntryManager weblogMgr = roller.getWeblogEntryManager();
-
-			WeblogEntry entry;
-			Document doc;
-			String handle;
-			Timestamp now = new Timestamp(new Date().getTime());
-			for (int i = offset; i < offset + limit; i++) {
-				doc = search.getSearcher().doc(hits[i].doc);
-				handle = doc.getField(FieldConstants.WEBSITE_HANDLE)
-						.stringValue();
-
-                entry = weblogMgr.getWeblogEntry(doc.getField(
-                        FieldConstants.ID).stringValue());
-
-				if (!(websiteSpecificSearch && handle.equals(feedRequest.getWeblogHandle()))
-                        && doc.getField(FieldConstants.CATEGORY) != null) {
-                    categorySet.add(doc.getField(FieldConstants.CATEGORY).stringValue());
-				}
-
-				// maybe null if search result returned inactive user
-				// or entry's user is not the requested user.
-				// but don't return future posts
-				if (entry != null && entry.getPubTime().before(now)) {
-					results.add(WeblogEntryWrapper.wrap(entry, urlStrategy));
-				}
-			}
-
-			if (!categorySet.isEmpty()) {
-				this.categories = categorySet;
-			}
-		} catch (IOException e) {
-			throw new WebloggerException(e);
-		}
-	}
 
 	/**
 	 * Get weblog being displayed.
@@ -274,10 +185,14 @@
 		return feedRequest.getWeblogCategoryName();
 	}
 
+	public String getErrorMessage() {
+		return errorMessage;
+	}
+
 	public WeblogCategoryWrapper getWeblogCategory() {
 		if (feedRequest.getWeblogCategory() != null) {
 			return WeblogCategoryWrapper.wrap(feedRequest.getWeblogCategory(),
-					urlStrategy);
+				urlStrategy);
 		}
 		return null;
 	}
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModel.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModel.java
index 9f7ebb2..3c3bd9a 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModel.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModel.java
@@ -18,7 +18,6 @@
 
 package org.apache.roller.weblogger.ui.rendering.model;
 
-import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Collections;
 import java.util.Date;
@@ -26,30 +25,19 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
-
 import org.apache.commons.text.StringEscapeUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.TopFieldDocs;
 import org.apache.roller.util.DateUtil;
 import org.apache.roller.weblogger.WebloggerException;
 import org.apache.roller.weblogger.business.URLStrategy;
-import org.apache.roller.weblogger.business.WeblogEntryManager;
-import org.apache.roller.weblogger.business.Weblogger;
 import org.apache.roller.weblogger.business.WebloggerFactory;
-import org.apache.roller.weblogger.business.search.FieldConstants;
 import org.apache.roller.weblogger.business.search.IndexManager;
-import org.apache.roller.weblogger.business.search.operations.SearchOperation;
-import org.apache.roller.weblogger.config.WebloggerRuntimeConfig;
-import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.business.search.SearchResultList;
 import org.apache.roller.weblogger.pojos.WeblogEntryWrapperComparator;
 import org.apache.roller.weblogger.pojos.wrapper.WeblogCategoryWrapper;
 import org.apache.roller.weblogger.pojos.wrapper.WeblogEntryWrapper;
 import org.apache.roller.weblogger.ui.rendering.pagers.SearchResultsPager;
 import org.apache.roller.weblogger.ui.rendering.pagers.WeblogEntriesPager;
 import org.apache.roller.weblogger.ui.rendering.util.WeblogSearchRequest;
-import org.apache.roller.weblogger.util.I18nMessages;
 
 /**
  * Extends normal page renderer model to represent search results.
@@ -65,7 +53,7 @@
 	private URLStrategy urlStrategy = null;
 
 	// the actual search results mapped by Day -> Set of entries
-    private final Map<Date, Set<WeblogEntryWrapper>> results = new TreeMap<>(Collections.reverseOrder());
+    private Map<Date, Set<WeblogEntryWrapper>> results = new TreeMap<>(Collections.reverseOrder());
 
 	// the pager used by the 3.0+ rendering system
 	private SearchResultsPager pager = null;
@@ -74,8 +62,7 @@
 	private int offset = 0;
 	private int limit = 0;
 	private Set<String> categories = new TreeSet<String>();
-	private boolean websiteSpecificSearch = true;
-	private String errorMessage = null;
+	private String errorMessage = "";
 
 	@Override
 	public void init(Map<String, Object> initData) throws WebloggerException {
@@ -83,8 +70,7 @@
 		// we expect the init data to contain a searchRequest object
 		searchRequest = (WeblogSearchRequest) initData.get("searchRequest");
 		if (searchRequest == null) {
-			throw new WebloggerException(
-					"expected searchRequest from init data");
+			throw new WebloggerException("expected searchRequest from init data");
 		}
 
 		// look for url strategy
@@ -98,55 +84,59 @@
 
 		// if there is no query, then we are done
 		if (searchRequest.getQuery() == null) {
-			pager = new SearchResultsPager(urlStrategy, searchRequest, results,
-					false);
+			pager = new SearchResultsPager(urlStrategy, searchRequest, results, false);
 			return;
 		}
 
 		// setup the search
-		IndexManager indexMgr = WebloggerFactory.getWeblogger()
-				.getIndexManager();
+		IndexManager indexMgr = WebloggerFactory.getWeblogger().getIndexManager();
+		try {
+			SearchResultList searchResultList = indexMgr.search(
+				searchRequest.getQuery(),
+				searchRequest.getWeblogHandle(),
+				searchRequest.getWeblogCategoryName(),
+				searchRequest.getLocale(),
+				searchRequest.getPageNum(),
+				RESULTS_PER_PAGE,
+				urlStrategy
+			);
+			hits = searchResultList.getResults().size();
+			offset = searchResultList.getOffset();
+			limit = searchResultList.getLimit();
+			categories = searchResultList.getCategories();
 
-		SearchOperation search = new SearchOperation(indexMgr);
-		search.setTerm(searchRequest.getQuery());
+			Timestamp now = new Timestamp(new Date().getTime());
+			for (WeblogEntryWrapper entry : searchResultList.getResults()) {
+				if (entry.getPubTime().before(now)) {
+					addEntryToResults(results, entry);
+				}
+			}
 
-		if (WebloggerRuntimeConfig.isSiteWideWeblog(searchRequest
-				.getWeblogHandle())) {
-			this.websiteSpecificSearch = false;
-		} else {
-			search.setWebsiteHandle(searchRequest.getWeblogHandle());
-		}
-
-		if (StringUtils.isNotEmpty(searchRequest.getWeblogCategoryName())) {
-			search.setCategory(searchRequest.getWeblogCategoryName());
-		}
-
-		if (searchRequest.getLocale() != null) {
-			search.setLocale(searchRequest.getLocale());
-		}
-
-		// execute search
-		indexMgr.executeIndexOperationNow(search);
-
-		if (search.getResultsCount() == -1) {
-			// this means there has been a parsing (or IO) error
-			this.errorMessage = I18nMessages.getMessages(
-					searchRequest.getLocaleInstance()).getString(
-					"error.searchProblem");
-		} else {
-
-			TopFieldDocs docs = search.getResults();
-			ScoreDoc[] hitsArr = docs.scoreDocs;
-			this.hits = search.getResultsCount();
-
-			// Convert the Hits into WeblogEntryData instances.
-			convertHitsToEntries(hitsArr, search);
-
+		} catch (WebloggerException we) {
+			errorMessage = we.getMessage();
 		}
 
 		// search completed, setup pager based on results
-		pager = new SearchResultsPager(urlStrategy, searchRequest, results,
-				(hits > (offset + limit)));
+		pager = new SearchResultsPager(
+			urlStrategy, searchRequest, results, (hits > (offset + limit)));
+	}
+
+	private void addEntryToResults(
+		Map<Date, Set<WeblogEntryWrapper>> results,
+		WeblogEntryWrapper entry) {
+
+		// convert entry's each date to midnight (00m 00h 00s)
+		Date midnight = DateUtil.getStartOfDay(entry.getPubTime());
+
+		// ensure we do not get duplicates from Lucene by
+		// using a Set Collection. Entries sorted by pubTime.
+		Set<WeblogEntryWrapper> set = results.get(midnight);
+		if (set == null) {
+			// date is not mapped yet, so we need a new Set
+			set = new TreeSet<>(new WeblogEntryWrapperComparator());
+			results.put(midnight, set);
+		}
+		set.add(entry);
 	}
 
 	/**
@@ -169,86 +159,6 @@
 		return pager;
 	}
 
-	/**
-	 * Convert hits to entries.
-	 * 
-	 * @param hits
-	 *            the hits
-	 * @param search
-	 *            the search
-	 * @throws WebloggerException
-	 *             the weblogger exception
-	 */
-	private void convertHitsToEntries(ScoreDoc[] hits, SearchOperation search)
-			throws WebloggerException {
-
-		// determine offset
-		this.offset = searchRequest.getPageNum() * RESULTS_PER_PAGE;
-		if (this.offset >= hits.length) {
-			this.offset = 0;
-		}
-
-		// determine limit
-		this.limit = RESULTS_PER_PAGE;
-		if (this.offset + this.limit > hits.length) {
-			this.limit = hits.length - this.offset;
-		}
-
-		try {
-			Set<String> categorySet = new TreeSet<>();
-			Weblogger roller = WebloggerFactory.getWeblogger();
-			WeblogEntryManager weblogMgr = roller.getWeblogEntryManager();
-
-			WeblogEntry entry;
-			Document doc;
-			String handle;
-			Timestamp now = new Timestamp(new Date().getTime());
-			for (int i = offset; i < offset + limit; i++) {
-				doc = search.getSearcher().doc(hits[i].doc);
-				handle = doc.getField(FieldConstants.WEBSITE_HANDLE)
-						.stringValue();
-
-                entry = weblogMgr.getWeblogEntry(doc.getField(
-                        FieldConstants.ID).stringValue());
-
-                if (!(websiteSpecificSearch && handle.equals(searchRequest.getWeblogHandle()))
-                        && doc.getField(FieldConstants.CATEGORY) != null) {
-                    categorySet.add(doc.getField(FieldConstants.CATEGORY).stringValue());
-                }
-
-				// maybe null if search result returned inactive user
-				// or entry's user is not the requested user.
-				// but don't return future posts
-				if (entry != null && entry.getPubTime().before(now)) {
-					addEntryToResults(WeblogEntryWrapper.wrap(entry,
-							urlStrategy));
-				}
-			}
-
-			if (!categorySet.isEmpty()) {
-				this.categories = categorySet;
-			}
-		} catch (IOException e) {
-			throw new WebloggerException(e);
-		}
-	}
-
-	private void addEntryToResults(WeblogEntryWrapper entry) {
-
-		// convert entry's each date to midnight (00m 00h 00s)
-		Date midnight = DateUtil.getStartOfDay(entry.getPubTime());
-
-		// ensure we do not get duplicates from Lucene by
-		// using a Set Collection. Entries sorted by pubTime.
-		Set<WeblogEntryWrapper> set = this.results.get(midnight);
-		if (set == null) {
-			// date is not mapped yet, so we need a new Set
-			set = new TreeSet<>(new WeblogEntryWrapperComparator());
-			this.results.put(midnight, set);
-		}
-		set.add(entry);
-	}
-
 	public String getTerm() {
 		String query = searchRequest.getQuery();
         return (query == null)
@@ -291,8 +201,7 @@
 	@Override
 	public WeblogCategoryWrapper getWeblogCategory() {
 		if (searchRequest.getWeblogCategory() != null) {
-			return WeblogCategoryWrapper.wrap(
-					searchRequest.getWeblogCategory(), urlStrategy);
+			return WeblogCategoryWrapper.wrap(searchRequest.getWeblogCategory(), urlStrategy);
 		}
 		return null;
 	}
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/util/WeblogSearchRequest.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/util/WeblogSearchRequest.java
index bf699f9..d1037ee 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/util/WeblogSearchRequest.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/util/WeblogSearchRequest.java
@@ -35,7 +35,7 @@
     
     private static Log log = LogFactory.getLog(WeblogSearchRequest.class);
     
-    private static final String SEARCH_SERVLET = "/roller-ui/rendering/search";
+    public static final String SEARCH_SERVLET = "/roller-ui/rendering/search";
     
     // lightweight attributes
     private String query = null;
diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/Maintenance.java b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/Maintenance.java
index 63c9443..30225ee 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/Maintenance.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/struts2/editor/Maintenance.java
@@ -28,7 +28,6 @@
 import org.apache.roller.weblogger.pojos.Weblog;
 import org.apache.roller.weblogger.ui.struts2.util.UIAction;
 import org.apache.roller.weblogger.util.cache.CacheManager;
-import org.apache.struts2.convention.annotation.AllowedMethods;
 
 /**
  * Allows user to perform maintenance operations such as flushing the page cache
@@ -58,7 +57,7 @@
         try {
             IndexManager manager = WebloggerFactory.getWeblogger()
                     .getIndexManager();
-            manager.rebuildWebsiteIndex(getActionWeblog());
+            manager.rebuildWeblogIndex(getActionWeblog());
 
             addMessage("maintenance.message.indexed");
         } catch (Exception ex) {
diff --git a/app/src/main/resources/log4j2.xml b/app/src/main/resources/log4j2.xml
index 4e21134..6b916d6 100644
--- a/app/src/main/resources/log4j2.xml
+++ b/app/src/main/resources/log4j2.xml
@@ -70,6 +70,7 @@
         
         <!-- roller.log; everything not defined here will end up in server.log -->
         <Logger name="org.apache.roller"       level="info"  additivity="false"> <AppenderRef ref="asyncRoller"/> </Logger>
+        <Logger name="org.apache.roller.weblogger.ui.struts2.util.UISecurityInterceptor" level="debug" additivity="false"> <AppenderRef ref="asyncRoller"/> </Logger>
         <Logger name="org.apache.velocity"     level="info"  additivity="false"> <AppenderRef ref="asyncRoller"/> </Logger>
         <Logger name="org.springframework"     level="info"  additivity="false"> <AppenderRef ref="asyncRoller"/> </Logger>
         <Logger name="org.apache.struts2"      level="info"  additivity="false"> <AppenderRef ref="asyncRoller"/> </Logger>
diff --git a/app/src/main/resources/rome.properties b/app/src/main/resources/rome.properties
index f544fb2..427ad37 100644
--- a/app/src/main/resources/rome.properties
+++ b/app/src/main/resources/rome.properties
@@ -18,7 +18,7 @@
 # Some RSS 0.91 feeds have pubDates in items
 
 WireFeedParser.classes=org.apache.roller.planet.util.rome.PlanetRSS091UParser \
-	                   org.apache.roller.planet.util.rome.PlanetRSS091NParser    
+	                   org.apache.roller.planet.util.rome.PlanetRSS091NParser
 	                                
 Converter.classes     =org.apache.roller.planet.util.rome.PlanetConverterForRSS091U \
 	                   org.apache.roller.planet.util.rome.PlanetConverterForRSS091N \
diff --git a/app/src/test/java/org/apache/roller/util/DerbyManager.java b/app/src/test/java/org/apache/roller/util/DerbyManager.java
index f28a6d4..ba6ef05 100644
--- a/app/src/test/java/org/apache/roller/util/DerbyManager.java
+++ b/app/src/test/java/org/apache/roller/util/DerbyManager.java
@@ -51,7 +51,7 @@
                 //System.setProperty("derby.drda.logConnections","true");
                 NetworkServerControl server = new NetworkServerControl();
                 server.start(new PrintWriter(System.out));
-                
+
                 try {Thread.sleep(2000);} catch (Exception ignored) {}
                 System.out.println("Runtime Info: " + server.getRuntimeInfo());
 
diff --git a/app/src/test/java/org/apache/roller/weblogger/business/IndexManagerTest.java b/app/src/test/java/org/apache/roller/weblogger/business/IndexManagerTest.java
deleted file mode 100644
index 617f008..0000000
--- a/app/src/test/java/org/apache/roller/weblogger/business/IndexManagerTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
-* Licensed to the Apache Software Foundation (ASF) under one or more
-*  contributor license agreements.  The ASF licenses this file to You
-* 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.  For additional information regarding
-* copyright in this work, please see the NOTICE file in the top level
-* directory of this distribution.
-*/
-package org.apache.roller.weblogger.business;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.roller.util.RollerConstants;
-import org.apache.roller.weblogger.TestUtils;
-import org.apache.roller.weblogger.business.search.IndexManager;
-import org.apache.roller.weblogger.business.search.IndexManagerImpl;
-import org.apache.roller.weblogger.business.search.operations.AddEntryOperation;
-import org.apache.roller.weblogger.business.search.operations.SearchOperation;
-import org.apache.roller.weblogger.pojos.User;
-import org.apache.roller.weblogger.pojos.Weblog;
-import org.apache.roller.weblogger.pojos.WeblogCategory;
-import org.apache.roller.weblogger.pojos.WeblogEntry;
-import org.apache.roller.weblogger.pojos.WeblogEntry.PubStatus;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.sql.Timestamp;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * Test Search Manager business layer operations.
- */
-public class IndexManagerTest  {
-    User testUser = null;
-    Weblog testWeblog = null;
-    public static Log log = LogFactory.getLog(IndexManagerTest.class);    
-
-    /**
-     * All tests in this suite require a user and a weblog.
-     */
-    @BeforeEach
-    public void setUp() throws Exception {
-        
-        // setup weblogger
-        TestUtils.setupWeblogger();
-        
-        try {
-            testUser = TestUtils.setupUser("entryTestUser");
-            testWeblog = TestUtils.setupWeblog("entryTestWeblog", testUser);
-            TestUtils.endSession(true);
-
-            //WeblogManager wmgr = WebloggerFactory.getWeblogger().getWeblogManager();
-            //assertEquals(1, wmgr.getWeblogCount());
- 
-        } catch (Exception ex) {
-            log.error("ERROR in test setup", ex);
-            throw new Exception("Test setup failed", ex);
-        }
-    }
-
-    @AfterEach
-    public void tearDown() throws Exception {
-        
-        try {
-            TestUtils.teardownWeblog(testWeblog.getId());
-            TestUtils.teardownUser(testUser.getUserName());
-            TestUtils.endSession(true);
-        } catch (Exception ex) {
-            log.error("ERROR in test teardown", ex);
-            throw new Exception("Test teardown failed", ex);
-        }
-    }
-
-    @Test
-    public void testSearch() throws Exception {
-        WeblogEntryManager wem = WebloggerFactory.getWeblogger().getWeblogEntryManager();
-
-        WeblogEntry wd1 = new WeblogEntry();            
-        wd1.setTitle("The Tholian Web");
-        wd1.setText(
-         "When the Enterprise attempts to ascertain the fate of the  "
-        +"U.S.S. Defiant which vanished 3 weeks ago, the warp engines  "
-        +"begin to lose power, and Spock reports strange sensor readings.");
-        wd1.setAnchor("dummy1");
-        wd1.setCreatorUserName(testUser.getUserName());
-        wd1.setStatus(PubStatus.PUBLISHED);
-        wd1.setUpdateTime(new Timestamp(System.currentTimeMillis()));
-        wd1.setPubTime(new Timestamp(System.currentTimeMillis()));
-        wd1.setWebsite(TestUtils.getManagedWebsite(testWeblog));
-
-        WeblogCategory cat = wem.getWeblogCategory(testWeblog.getWeblogCategory("General").getId());
-        wd1.setCategory(cat);
-
-        wem.saveWeblogEntry(wd1);
-        TestUtils.endSession(true);
-        wd1 = TestUtils.getManagedWeblogEntry(wd1);
-
-        IndexManager imgr = WebloggerFactory.getWeblogger().getIndexManager();
-        imgr.executeIndexOperationNow(
-            new AddEntryOperation(WebloggerFactory.getWeblogger(), (IndexManagerImpl)imgr, wd1));
-
-        WeblogEntry wd2 = new WeblogEntry();
-        wd2.setTitle("A Piece of the Action");
-        wd2.setText(
-          "The crew of the Enterprise attempts to make contact with "
-          +"the inhabitants of planet Sigma Iotia II, and Uhura puts Kirk "
-          +"in communication with Boss Oxmyx.");
-        wd2.setAnchor("dummy2");
-        wd2.setStatus(PubStatus.PUBLISHED);
-        wd2.setCreatorUserName(testUser.getUserName());
-        wd2.setUpdateTime(new Timestamp(System.currentTimeMillis()));
-        wd2.setPubTime(new Timestamp(System.currentTimeMillis()));
-        wd2.setWebsite(TestUtils.getManagedWebsite(testWeblog));
-
-        cat = wem.getWeblogCategory(testWeblog.getWeblogCategory("General").getId());
-        wd2.setCategory(cat);
-
-        wem.saveWeblogEntry(wd2);
-        TestUtils.endSession(true);
-        wd2 = TestUtils.getManagedWeblogEntry(wd2);
-
-         imgr.executeIndexOperationNow(
-             new AddEntryOperation(WebloggerFactory.getWeblogger(), (IndexManagerImpl)imgr, wd2));
-
-        Thread.sleep(RollerConstants.SEC_IN_MS);
-
-        SearchOperation search = new SearchOperation(imgr);
-        search.setTerm("Enterprise");
-        imgr.executeIndexOperationNow(search);
-        assertEquals(2, search.getResultsCount());
-
-        SearchOperation search2 = new SearchOperation(imgr);
-        search2.setTerm("Tholian");
-        imgr.executeIndexOperationNow(search2);
-        assertEquals(1, search2.getResultsCount());
-
-        // Clean up
-        imgr.removeEntryIndexOperation(wd1);
-        imgr.removeEntryIndexOperation(wd2);
-
-        SearchOperation search3 = new SearchOperation(imgr);
-        search3.setTerm("Enterprise");
-        imgr.executeIndexOperationNow(search3);
-        assertEquals(0, search3.getResultsCount());
-    }    
-}
diff --git a/app/src/test/java/org/apache/roller/weblogger/business/search/IndexManagerTest.java b/app/src/test/java/org/apache/roller/weblogger/business/search/IndexManagerTest.java
new file mode 100644
index 0000000..7f146b0
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/business/search/IndexManagerTest.java
@@ -0,0 +1,158 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+*  contributor license agreements.  The ASF licenses this file to You
+* 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.  For additional information regarding
+* copyright in this work, please see the NOTICE file in the top level
+* directory of this distribution.
+*/
+package org.apache.roller.weblogger.business.search;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.List;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.roller.util.RollerConstants;
+import org.apache.roller.weblogger.TestUtils;
+import org.apache.roller.weblogger.business.WeblogEntryManager;
+import org.apache.roller.weblogger.business.WebloggerFactory;
+import org.apache.roller.weblogger.pojos.User;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogCategory;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.instancio.Instancio;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.roller.weblogger.ui.rendering.model.SearchResultsModel.RESULTS_PER_PAGE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Test Search Manager business layer operations.
+ */
+public class IndexManagerTest {
+    User testUser = null;
+    Weblog testWeblog = null;
+    public static Log log = LogFactory.getLog(IndexManagerTest.class);
+
+    /**
+     * All tests in this suite require a user and a weblog.
+     */
+    @BeforeEach
+    public void setUp() throws Exception {
+        
+        // setup weblogger
+        TestUtils.setupWeblogger();
+        
+        try {
+            testUser = TestUtils.setupUser("entrytestuser");
+            testWeblog = TestUtils.setupWeblog("entrytestweblog", testUser);
+            TestUtils.endSession(true);
+        } catch (Exception ex) {
+            log.error("ERROR in test setup", ex);
+            throw new Exception("Test setup failed", ex);
+        }
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        
+        try {
+            TestUtils.teardownWeblog(testWeblog.getId());
+            TestUtils.teardownUser(testUser.getUserName());
+            TestUtils.endSession(true);
+        } catch (Exception ex) {
+            log.error("ERROR in test teardown", ex);
+            throw new Exception("Test teardown failed", ex);
+        }
+    }
+
+    @Test
+    public void testBasicOperation() throws Exception {
+
+        IndexManager indexManager = WebloggerFactory.getWeblogger().getIndexManager();
+        WeblogEntryManager entryManager = WebloggerFactory.getWeblogger().getWeblogEntryManager();
+
+        List<WeblogEntry> entries = createWeblogEntries(testWeblog, indexManager, entryManager);
+
+        try {
+            SearchResultList result = indexManager.search("Enterprise",
+                testWeblog.getHandle(), null, testWeblog.getLocale(), 0, RESULTS_PER_PAGE,
+                WebloggerFactory.getWeblogger().getUrlStrategy());
+            assertEquals(2, result.getResults().size());
+
+            result = indexManager.search("Tholian",
+                testWeblog.getHandle(), null, testWeblog.getLocale(), 0, RESULTS_PER_PAGE,
+                WebloggerFactory.getWeblogger().getUrlStrategy());
+            assertEquals(1, result.getResults().size());
+
+        } finally {
+            for (WeblogEntry entry : entries) {
+                indexManager.removeEntryIndexOperation(TestUtils.getManagedWeblogEntry(entry));
+            }
+            indexManager.removeWeblogIndex(testWeblog);
+        }
+    }
+
+    /**
+     * Create some weblog entries, two with some Star Trek content
+     */
+    public static List<WeblogEntry> createWeblogEntries(
+        Weblog testWeblog,
+        IndexManager indexManager,
+        WeblogEntryManager entryManager) throws Exception {
+
+        List<WeblogEntry> entries = Instancio.ofList(WeblogEntry.class).size(10).create();
+
+        entries.get(0).setTitle("The Tholian Web");
+        entries.get(0).setPubTime(new Timestamp(System.currentTimeMillis()));
+        entries.get(0).setText(
+            "When the Enterprise attempts to ascertain the fate of the  "
+                +"U.S.S. Defiant which vanished 3 weeks ago, the warp engines  "
+                +"begin to lose power, and Spock reports strange sensor readings.");
+
+        Thread.sleep(500);
+
+        entries.get(1).setTitle("A Piece of the Action");
+        entries.get(1).setPubTime(new Timestamp(System.currentTimeMillis()));
+        entries.get(1).setText(
+            "The crew of the Enterprise attempts to make contact with "
+                +"the inhabitants of planet Sigma Iotia II, and Uhura puts Kirk "
+                +"in communication with Boss Oxmyx.");
+
+        // save and index those entries
+
+        for (WeblogEntry entry : entries) {
+
+            // fill in relationship fields to make JPA happy
+
+            WeblogCategory cat = entryManager.getWeblogCategory(
+                testWeblog.getWeblogCategory("General").getId());
+            entry.setCategory(cat);
+            entry.setWebsite(TestUtils.getManagedWebsite(testWeblog));
+            entry.setEntryAttributes(Collections.emptySet());
+            entry.setTags(Collections.emptySet());
+
+            entry.setLocale(testWeblog.getLocale());
+
+            entryManager.saveWeblogEntry(entry);
+            TestUtils.endSession(true);
+
+            indexManager.addEntryIndexOperation(entry);
+        }
+
+        Thread.sleep(RollerConstants.SEC_IN_MS);
+        return entries;
+    }
+}
diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModelTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModelTest.java
new file mode 100644
index 0000000..1ca19b1
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsFeedModelTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  The ASF licenses this file to You
+ * 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.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+
+/* Created on March 8, 2023 */
+
+package org.apache.roller.weblogger.ui.rendering.model;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.roller.weblogger.TestUtils;
+import org.apache.roller.weblogger.WebloggerException;
+import org.apache.roller.weblogger.business.WeblogEntryManager;
+import org.apache.roller.weblogger.business.WebloggerFactory;
+import org.apache.roller.weblogger.business.search.IndexManagerTest;
+import org.apache.roller.weblogger.business.search.IndexManager;
+import org.apache.roller.weblogger.pojos.User;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.ui.rendering.util.WeblogFeedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.roller.weblogger.business.search.IndexManagerTest.createWeblogEntries;
+import static org.apache.roller.weblogger.ui.rendering.util.WeblogSearchRequest.SEARCH_SERVLET;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class SearchResultsFeedModelTest {
+    User testUser = null;
+    Weblog testWeblog = null;
+    public static Log log = LogFactory.getLog(IndexManagerTest.class);
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        TestUtils.setupWeblogger();
+        try {
+            testUser = TestUtils.setupUser("entrytestuser");
+            testWeblog = TestUtils.setupWeblog("entrytestweblog", testUser);
+            TestUtils.endSession(true);
+        } catch (Exception ex) {
+            log.error("ERROR in test setup", ex);
+            throw new Exception("Test setup failed", ex);
+        }
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        try {
+            TestUtils.teardownWeblog(testWeblog.getId());
+            TestUtils.teardownUser(testUser.getUserName());
+            TestUtils.endSession(true);
+        } catch (Exception ex) {
+            log.error("ERROR in test teardown", ex);
+            throw new Exception("Test teardown failed", ex);
+        }
+    }
+
+    @Test
+    void testBasicOperation() throws Exception {
+
+        IndexManager indexManager =
+            WebloggerFactory.getWeblogger().getIndexManager();
+        WeblogEntryManager entryManager = WebloggerFactory.getWeblogger().getWeblogEntryManager();
+
+        List<WeblogEntry> entries = createWeblogEntries(testWeblog, indexManager, entryManager);
+
+        try {
+            SearchResultsFeedModel model = executeSearch("Enterprise");
+            assertEquals(2, model.getResults().size());
+
+            model = executeSearch("Tholian");
+            assertEquals(1, model.getResults().size());
+
+        } finally {
+            for (WeblogEntry entry : entries) {
+                indexManager.removeEntryIndexOperation(TestUtils.getManagedWeblogEntry(entry));
+            }
+            indexManager.removeWeblogIndex(testWeblog);
+        }
+    }
+
+    SearchResultsFeedModel executeSearch(String term) throws WebloggerException {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getServletPath()).thenReturn(SEARCH_SERVLET);
+        when(request.getRequestURL()).thenReturn(
+            new StringBuffer(String.format("http://localhost/%s", SEARCH_SERVLET)));
+        when(request.getPathInfo()).thenReturn(null);
+
+//        WeblogSearchRequest searchRequest = new WeblogSearchRequest(request);
+//        searchRequest.setWeblogHandle(testWeblog.getHandle());
+//        searchRequest.setQuery(term);
+
+        WeblogFeedRequest feedRequest = new WeblogFeedRequest();
+        feedRequest.setWeblog(testWeblog);
+        feedRequest.setTerm(term);
+
+        Map<String, Object> initData = new HashMap<>();
+        //initData.put("searchRequest", searchRequest);
+        initData.put("parsedRequest", feedRequest);
+
+        SearchResultsFeedModel model = new SearchResultsFeedModel();
+        model.init(initData);
+        return model;
+    }
+
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModelTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModelTest.java
new file mode 100644
index 0000000..9c38e3b
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/model/SearchResultsModelTest.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  The ASF licenses this file to You
+ * 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.  For additional information regarding
+ * copyright in this work, please see the NOTICE file in the top level
+ * directory of this distribution.
+ */
+
+/* Created on March 8, 2023 */
+
+package org.apache.roller.weblogger.ui.rendering.model;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.roller.weblogger.TestUtils;
+import org.apache.roller.weblogger.WebloggerException;
+import org.apache.roller.weblogger.business.WeblogEntryManager;
+import org.apache.roller.weblogger.business.WebloggerFactory;
+import org.apache.roller.weblogger.business.search.IndexManagerTest;
+import org.apache.roller.weblogger.business.search.IndexManager;
+import org.apache.roller.weblogger.pojos.User;
+import org.apache.roller.weblogger.pojos.Weblog;
+import org.apache.roller.weblogger.pojos.WeblogEntry;
+import org.apache.roller.weblogger.pojos.wrapper.WeblogEntryWrapper;
+import org.apache.roller.weblogger.ui.rendering.util.WeblogPageRequest;
+import org.apache.roller.weblogger.ui.rendering.util.WeblogSearchRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.roller.weblogger.business.search.IndexManagerTest.createWeblogEntries;
+import static org.apache.roller.weblogger.ui.rendering.util.WeblogSearchRequest.SEARCH_SERVLET;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class SearchResultsModelTest {
+    User testUser = null;
+    Weblog testWeblog = null;
+    public static Log log = LogFactory.getLog(IndexManagerTest.class);
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        TestUtils.setupWeblogger();
+        try {
+            testUser = TestUtils.setupUser("entrytestuser");
+            testWeblog = TestUtils.setupWeblog("entrytestweblog", testUser);
+            TestUtils.endSession(true);
+        } catch (Exception ex) {
+            log.error("ERROR in test setup", ex);
+            throw new Exception("Test setup failed", ex);
+        }
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        try {
+            TestUtils.teardownWeblog(testWeblog.getId());
+            TestUtils.teardownUser(testUser.getUserName());
+            TestUtils.endSession(true);
+        } catch (Exception ex) {
+            log.error("ERROR in test teardown", ex);
+            throw new Exception("Test teardown failed", ex);
+        }
+    }
+
+    @Test
+    void testBasicOperation() throws Exception {
+
+        IndexManager indexManager = WebloggerFactory.getWeblogger().getIndexManager();
+        WeblogEntryManager entryManager = WebloggerFactory.getWeblogger().getWeblogEntryManager();
+
+        List<WeblogEntry> entries = createWeblogEntries(testWeblog, indexManager, entryManager);
+
+        try {
+            SearchResultsModel model = executeSearch("Enterprise");
+            assertEquals(1, model.getResults().size());
+            int count = 0;
+            for (Date midnight : model.getResults().keySet()) {
+                Set<WeblogEntryWrapper> wrappers = model.getResults().get(midnight);
+                count += wrappers.size();
+            }
+            assertEquals(2, count);
+
+            model = executeSearch("Tholian");
+            assertEquals(1, model.getResults().size());
+            count = 0;
+            for (Date midnight : model.getResults().keySet()) {
+                Set<WeblogEntryWrapper> wrappers = model.getResults().get(midnight);
+                count += wrappers.size();
+            }
+            assertEquals(1, count);
+
+        } finally {
+            for (WeblogEntry entry : entries) {
+                indexManager.removeEntryIndexOperation(TestUtils.getManagedWeblogEntry(entry));
+            }
+            indexManager.removeWeblogIndex(testWeblog);
+        }
+    }
+
+    SearchResultsModel executeSearch(String term) throws WebloggerException {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getServletPath()).thenReturn(SEARCH_SERVLET);
+        when(request.getRequestURL()).thenReturn(
+            new StringBuffer(String.format("http://localhost/%s", SEARCH_SERVLET)));
+        when(request.getPathInfo()).thenReturn(null);
+
+        WeblogSearchRequest searchRequest = new WeblogSearchRequest(request);
+        searchRequest.setWeblogHandle(testWeblog.getHandle());
+        searchRequest.setQuery(term);
+
+        WeblogPageRequest pageRequest = new WeblogPageRequest();
+
+        Map<String, Object> initData = new HashMap<>();
+        initData.put("searchRequest", searchRequest);
+        initData.put("parsedRequest", pageRequest);
+        initData.put("pageRequest", pageRequest);
+
+        SearchResultsModel model = new SearchResultsModel();
+        model.init(initData);
+        return model;
+    }
+
+
+}
\ No newline at end of file
diff --git a/app/src/test/resources/log4j2.xml b/app/src/test/resources/log4j2.xml
index b2c3181..1967b03 100644
--- a/app/src/test/resources/log4j2.xml
+++ b/app/src/test/resources/log4j2.xml
@@ -52,7 +52,7 @@
     <Loggers>
         
         <!-- roller.log; everything not defined here will end up in server.log -->
-        <Logger name="org.apache.roller"   level="info"  additivity="false"> <AppenderRef ref="roller"/> </Logger>
+        <Logger name="org.apache.roller"   level="debug"  additivity="false"> <AppenderRef ref="roller"/> </Logger>
         <Logger name="org.apache.velocity" level="info"  additivity="false"> <AppenderRef ref="roller"/> </Logger>
         <Logger name="org.springframework" level="info"  additivity="false"> <AppenderRef ref="roller"/> </Logger>
         <Logger name="org.apache.struts2"  level="debug" additivity="false"> <AppenderRef ref="roller"/> </Logger>