diff --git a/src/main/java/org/codelibs/fess/api/BaseApiManager.java b/src/main/java/org/codelibs/fess/api/BaseApiManager.java
index 75a216acc47a8e22a4cff002d3eb6037f2e3c3db..965caf935fabd655209c7314f4fc9dea8ba9af46 100644
--- a/src/main/java/org/codelibs/fess/api/BaseApiManager.java
+++ b/src/main/java/org/codelibs/fess/api/BaseApiManager.java
@@ -33,14 +33,12 @@ public abstract class BaseApiManager implements WebApiManager {
 
     protected static final String HOT_SEARCH_WORD_API = "/hotSearchWordApi";
 
-    protected static final String SUGGEST_API = "/suggestApi";
-
     protected static final String SEARCH_API = "/searchApi";
 
     protected String pathPrefix;
 
     protected static enum FormatType {
-        SEARCH, LABEL, SUGGEST, HOTSEARCHWORD, FAVORITE, FAVORITES, OTHER, PING;
+        SEARCH, LABEL, HOTSEARCHWORD, FAVORITE, FAVORITES, OTHER, PING;
     }
 
     public String getPathPrefix() {
@@ -60,8 +58,6 @@ public abstract class BaseApiManager implements WebApiManager {
             return FormatType.SEARCH;
         } else if (FormatType.LABEL.name().equals(type)) {
             return FormatType.LABEL;
-        } else if (FormatType.SUGGEST.name().equals(type)) {
-            return FormatType.SUGGEST;
         } else if (FormatType.HOTSEARCHWORD.name().equals(type)) {
             return FormatType.HOTSEARCHWORD;
         } else if (FormatType.FAVORITE.name().equals(type)) {
diff --git a/src/main/java/org/codelibs/fess/api/es/EsApiManager.java b/src/main/java/org/codelibs/fess/api/es/EsApiManager.java
index 8edfacf1e4e7840a9fb3d9ab79c7c3691ebbb19e..ab974851c030cdf82af3463baa8ca25c5513a54c 100644
--- a/src/main/java/org/codelibs/fess/api/es/EsApiManager.java
+++ b/src/main/java/org/codelibs/fess/api/es/EsApiManager.java
@@ -39,23 +39,24 @@ public class EsApiManager extends BaseApiManager {
     }
 
     @Override
-    public boolean matches(HttpServletRequest request) {
+    public boolean matches(final HttpServletRequest request) {
         final String servletPath = request.getServletPath();
         if (servletPath.startsWith(pathPrefix)) {
-            FessLoginAssist loginAssist = ComponentUtil.getLoginAssist();
+            final FessLoginAssist loginAssist = ComponentUtil.getLoginAssist();
             return loginAssist.getSessionUserBean().map(user -> user.hasRoles(acceptedRoles)).orElseGet(() -> Boolean.FALSE).booleanValue();
         }
         return false;
     }
 
     @Override
-    public void process(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+    public void process(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException,
+            ServletException {
         String path = request.getServletPath().substring(pathPrefix.length());
         if (!path.startsWith("/")) {
             path = "/" + path;
         }
-        Method httpMethod = Method.valueOf(request.getMethod().toUpperCase(Locale.ROOT));
-        CurlRequest curlRequest = new CurlRequest(httpMethod, getUrl() + path);
+        final Method httpMethod = Method.valueOf(request.getMethod().toUpperCase(Locale.ROOT));
+        final CurlRequest curlRequest = new CurlRequest(httpMethod, getUrl() + path);
         request.getParameterMap().entrySet().stream().forEach(entry -> {
             if (entry.getValue().length > 1) {
                 curlRequest.param(entry.getKey(), String.join(",", entry.getValue()));
@@ -68,7 +69,7 @@ public class EsApiManager extends BaseApiManager {
             if (httpMethod != Method.GET) {
                 try (ServletInputStream in = request.getInputStream(); OutputStream out = con.getOutputStream()) {
                     CopyUtil.copy(in, out);
-                } catch (IOException e) {
+                } catch (final IOException e) {
                     throw new IORuntimeException(e);
                 }
             }
@@ -76,17 +77,17 @@ public class EsApiManager extends BaseApiManager {
             try (InputStream in = con.getInputStream(); ServletOutputStream out = response.getOutputStream()) {
                 response.setStatus(con.getResponseCode());
                 CopyUtil.copy(in, out);
-            } catch (IOException e) {
+            } catch (final IOException e) {
                 try (InputStream err = con.getErrorStream()) {
                     logger.error(new String(InputStreamUtil.getBytes(err), Constants.CHARSET_UTF_8));
-                } catch (IOException e1) {}
+                } catch (final IOException e1) {}
                 throw new IORuntimeException(e);
             }
         });
         // TODO exception
     }
 
-    public void setAcceptedRoles(String[] acceptedRoles) {
+    public void setAcceptedRoles(final String[] acceptedRoles) {
         this.acceptedRoles = acceptedRoles;
     }
 
diff --git a/src/main/java/org/codelibs/fess/api/json/JsonApiManager.java b/src/main/java/org/codelibs/fess/api/json/JsonApiManager.java
index 8c13bec66d65adc2de343956a36a383e65a46f8c..c365333ce02dfb918a612c20904d1ca98f531285 100644
--- a/src/main/java/org/codelibs/fess/api/json/JsonApiManager.java
+++ b/src/main/java/org/codelibs/fess/api/json/JsonApiManager.java
@@ -18,11 +18,15 @@ package org.codelibs.fess.api.json;
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.net.URLDecoder;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import javax.annotation.Resource;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -31,18 +35,28 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.codelibs.core.CoreLibConstants;
 import org.codelibs.core.lang.StringUtil;
+import org.codelibs.core.misc.DynamicProperties;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.api.BaseApiManager;
-import org.codelibs.fess.api.WebApiRequest;
-import org.codelibs.fess.api.WebApiResponse;
+import org.codelibs.fess.app.service.FavoriteLogService;
+import org.codelibs.fess.app.service.SearchService;
+import org.codelibs.fess.entity.FacetInfo;
+import org.codelibs.fess.entity.GeoInfo;
 import org.codelibs.fess.entity.PingResponse;
+import org.codelibs.fess.entity.SearchRenderData;
+import org.codelibs.fess.entity.SearchRequestParams;
 import org.codelibs.fess.es.client.FessEsClient;
 import org.codelibs.fess.exception.WebApiException;
+import org.codelibs.fess.helper.FieldHelper;
+import org.codelibs.fess.helper.HotSearchWordHelper;
+import org.codelibs.fess.helper.HotSearchWordHelper.Range;
+import org.codelibs.fess.helper.LabelTypeHelper;
+import org.codelibs.fess.helper.QueryHelper;
+import org.codelibs.fess.helper.UserInfoHelper;
 import org.codelibs.fess.util.ComponentUtil;
+import org.codelibs.fess.util.DocumentUtil;
 import org.codelibs.fess.util.FacetResponse;
 import org.codelibs.fess.util.FacetResponse.Field;
-import org.codelibs.fess.util.MoreLikeThisResponse;
-import org.codelibs.fess.util.WebApiUtil;
 import org.lastaflute.web.util.LaRequestUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,6 +65,9 @@ public class JsonApiManager extends BaseApiManager {
 
     private static final Logger logger = LoggerFactory.getLogger(JsonApiManager.class);
 
+    @Resource
+    protected DynamicProperties crawlerProperties;
+
     public JsonApiManager() {
         setPathPrefix("/json");
     }
@@ -76,9 +93,6 @@ public class JsonApiManager extends BaseApiManager {
         case LABEL:
             processLabelRequest(request, response, chain);
             break;
-        case SUGGEST:
-            processSuggestRequest(request, response, chain);
-            break;
         case HOTSEARCHWORD:
             processHotSearchWordRequest(request, response, chain);
             break;
@@ -119,26 +133,26 @@ public class JsonApiManager extends BaseApiManager {
     }
 
     protected void processSearchRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
+        final SearchService searchService = ComponentUtil.getComponent(SearchService.class);
+
         int status = 0;
         String errMsg = StringUtil.EMPTY;
         String query = null;
         final StringBuilder buf = new StringBuilder(1000);
         request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
-        final String queryId = request.getParameter("queryId");
         try {
-            chain.doFilter(new WebApiRequest(request, SEARCH_API), new WebApiResponse(response));
-            WebApiUtil.validate();
-            query = WebApiUtil.getObject("searchQuery");
-            final String execTime = WebApiUtil.getObject("execTime");
-            final String queryTime = WebApiUtil.getObject("queryTime");
-            final String searchTime = WebApiUtil.getObject("searchTime");
-            final String pageSize = WebApiUtil.getObject("pageSize");
-            final String currentPageNumber = WebApiUtil.getObject("currentPageNumber");
-            final String allRecordCount = WebApiUtil.getObject("allRecordCount");
-            final String allPageCount = WebApiUtil.getObject("allPageCount");
-            final List<Map<String, Object>> documentItems = WebApiUtil.getObject("documentItems");
-            final FacetResponse facetResponse = WebApiUtil.getObject("facetResponse");
-            final MoreLikeThisResponse moreLikeThisResponse = WebApiUtil.getObject("moreLikeThisResponse");
+            final SearchRenderData data = new SearchRenderData();
+            final SearchApiRequestParams params = new SearchApiRequestParams(request);
+            searchService.search(request, params, data);
+            query = params.getQuery();
+            final String execTime = data.getExecTime();
+            final String queryTime = Long.toString(data.getQueryTime());
+            final String pageSize = Integer.toString(data.getPageSize());
+            final String currentPageNumber = Integer.toString(data.getCurrentPageNumber());
+            final String allRecordCount = Long.toString(data.getAllRecordCount());
+            final String allPageCount = Integer.toString(data.getAllPageCount());
+            final List<Map<String, Object>> documentItems = data.getDocumentItems();
+            final FacetResponse facetResponse = data.getFacetResponse();
 
             buf.append("\"query\":");
             buf.append(escapeJson(query));
@@ -146,14 +160,7 @@ public class JsonApiManager extends BaseApiManager {
             buf.append(execTime);
             buf.append(",\"queryTime\":");
             buf.append(queryTime);
-            buf.append(",\"searchTime\":");
-            buf.append(searchTime);
             buf.append(',');
-            if (StringUtil.isNotBlank(queryId)) {
-                buf.append("\"queryId\":");
-                buf.append(escapeJson(queryId));
-                buf.append(',');
-            }
             buf.append("\"pageSize\":");
             buf.append(pageSize);
             buf.append(',');
@@ -248,46 +255,6 @@ public class JsonApiManager extends BaseApiManager {
                     buf.append(']');
                 }
             }
-            if (moreLikeThisResponse != null && !moreLikeThisResponse.isEmpty()) {
-                buf.append(',');
-                buf.append("\"moreLikeThis\":[");
-                boolean first = true;
-                for (final Map.Entry<String, List<Map<String, Object>>> mltEntry : moreLikeThisResponse.entrySet()) {
-                    if (!first) {
-                        buf.append(',');
-                    } else {
-                        first = false;
-                    }
-                    buf.append("{\"id\":");
-                    buf.append(escapeJson(mltEntry.getKey()));
-                    buf.append(",\"result\":[");
-                    boolean first1 = true;
-                    for (final Map<String, Object> document : mltEntry.getValue()) {
-                        if (!first1) {
-                            buf.append(',');
-                        } else {
-                            first1 = false;
-                        }
-                        buf.append('{');
-                        boolean first2 = true;
-                        for (final Map.Entry<String, Object> entry : document.entrySet()) {
-                            if (StringUtil.isNotBlank(entry.getKey()) && entry.getValue() != null) {
-                                if (!first2) {
-                                    buf.append(',');
-                                } else {
-                                    first2 = false;
-                                }
-                                buf.append(escapeJson(entry.getKey()));
-                                buf.append(':');
-                                buf.append(escapeJson(entry.getValue()));
-                            }
-                        }
-                        buf.append('}');
-                    }
-                    buf.append("]}");
-                }
-                buf.append(']');
-            }
         } catch (final Exception e) {
             status = 1;
             errMsg = e.getMessage();
@@ -304,11 +271,13 @@ public class JsonApiManager extends BaseApiManager {
     }
 
     protected void processLabelRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
+        final LabelTypeHelper labelTypeHelper = ComponentUtil.getLabelTypeHelper();
+
         int status = 0;
         String errMsg = StringUtil.EMPTY;
         final StringBuilder buf = new StringBuilder(255);
         try {
-            final List<Map<String, String>> labelTypeItems = ComponentUtil.getLabelTypeHelper().getLabelTypeItemList();
+            final List<Map<String, String>> labelTypeItems = labelTypeHelper.getLabelTypeItemList();
             buf.append("\"recordCount\":");
             buf.append(labelTypeItems.size());
             if (!labelTypeItems.isEmpty()) {
@@ -341,91 +310,20 @@ public class JsonApiManager extends BaseApiManager {
 
     }
 
-    protected void processSuggestRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
-        // TODO
-        //        int status = 0;
-        //        String errMsg = StringUtil.EMPTY;
-        //        final StringBuilder buf = new StringBuilder(255);
-        //        try {
-        //            chain.doFilter(new WebApiRequest(request, SUGGEST_API), new WebApiResponse(response));
-        //            WebApiUtil.validate();
-        //            final Integer suggestRecordCount = WebApiUtil.getObject("suggestRecordCount");
-        //            final List<SuggestResponse> suggestResultList = WebApiUtil.getObject("suggestResultList");
-        //            final List<String> suggestFieldName = WebApiUtil.getObject("suggestFieldName");
-        //
-        //            buf.append("\"recordCount\":");
-        //            buf.append(suggestRecordCount);
-        //
-        //            if (suggestResultList.size() > 0) {
-        //                buf.append(',');
-        //                buf.append("\"result\":[");
-        //                boolean first1 = true;
-        //                for (int i = 0; i < suggestResultList.size(); i++) {
-        //
-        //                    final SuggestResponse suggestResponse = suggestResultList.get(i);
-        //
-        //                    for (final Map.Entry<String, List<String>> entry : suggestResponse.entrySet()) {
-        //                        final String fn = suggestFieldName.get(i);
-        //                        if (!first1) {
-        //                            buf.append(',');
-        //                        } else {
-        //                            first1 = false;
-        //                        }
-        //
-        //                        final SuggestResponseList srList = (SuggestResponseList) entry.getValue();
-        //
-        //                        buf.append("{\"token\":");
-        //                        buf.append(escapeJson(entry.getKey()));
-        //                        buf.append(", \"fn\":");
-        //                        buf.append(escapeJson(fn));
-        //                        buf.append(", \"startOffset\":");
-        //                        buf.append(Integer.toString(srList.getStartOffset()));
-        //                        buf.append(", \"endOffset\":");
-        //                        buf.append(Integer.toString(srList.getEndOffset()));
-        //                        buf.append(", \"numFound\":");
-        //                        buf.append(Integer.toString(srList.getNumFound()));
-        //                        buf.append(", ");
-        //                        buf.append("\"result\":[");
-        //                        boolean first2 = true;
-        //                        for (final String value : srList) {
-        //                            if (!first2) {
-        //                                buf.append(',');
-        //                            } else {
-        //                                first2 = false;
-        //                            }
-        //                            buf.append(escapeJson(value));
-        //                        }
-        //                        buf.append("]}");
-        //                    }
-        //
-        //                }
-        //                buf.append(']');
-        //            }
-        //        } catch (final Exception e) {
-        //            if (e instanceof WebApiException) {
-        //                status = ((WebApiException) e).getStatusCode();
-        //            } else {
-        //                status = 1;
-        //            }
-        //            errMsg = e.getMessage();
-        //            if (logger.isDebugEnabled()) {
-        //                logger.debug("Failed to process a suggest request.", e);
-        //            }
-        //        }
-        //
-        //        writeJsonResponse(status, buf.toString(), errMsg);
-
-    }
-
     protected void processHotSearchWordRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
+        if (Constants.FALSE.equals(crawlerProperties.getProperty(Constants.WEB_API_HOT_SEARCH_WORD_PROPERTY, Constants.TRUE))) {
+            writeJsonResponse(9, null, "Unsupported operation.");
+            return;
+        }
+
+        final HotSearchWordHelper hotSearchWordHelper = ComponentUtil.getHotSearchWordHelper();
 
         int status = 0;
         String errMsg = StringUtil.EMPTY;
         final StringBuilder buf = new StringBuilder(255);
         try {
-            chain.doFilter(new WebApiRequest(request, HOT_SEARCH_WORD_API), new WebApiResponse(response));
-            WebApiUtil.validate();
-            final List<String> hotSearchWordList = WebApiUtil.getObject("hotSearchWordList");
+            final List<String> hotSearchWordList =
+                    hotSearchWordHelper.getHotSearchWordList(Range.parseRange(request.getParameter("range")));
 
             buf.append("\"result\":[");
             boolean first1 = true;
@@ -455,39 +353,128 @@ public class JsonApiManager extends BaseApiManager {
     }
 
     protected void processFavoriteRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
-        int status = 0;
-        String body = null;
-        String errMsg = null;
+        if (Constants.FALSE.equals(crawlerProperties.getProperty(Constants.USER_FAVORITE_PROPERTY, Constants.FALSE))) {
+            writeJsonResponse(9, null, "Unsupported operation.");
+            return;
+        }
+
+        final UserInfoHelper userInfoHelper = ComponentUtil.getUserInfoHelper();
+        final FieldHelper fieldHelper = ComponentUtil.getFieldHelper();
+        final SearchService searchService = ComponentUtil.getComponent(SearchService.class);
+        final FavoriteLogService favoriteLogService = ComponentUtil.getComponent(FavoriteLogService.class);
+
         try {
-            chain.doFilter(new WebApiRequest(request, FAVORITE_API), new WebApiResponse(response));
-            WebApiUtil.validate();
+            final String docId = request.getParameter("docId");
+            final String queryId = request.getParameter("queryId");
+
+            final String[] docIds = userInfoHelper.getResultDocIds(URLDecoder.decode(queryId, Constants.UTF_8));
+            if (docIds == null) {
+                throw new WebApiException(6, "No searched urls.");
+            }
+
+            searchService
+                    .getDocumentByDocId(docId, new String[] { fieldHelper.idField, fieldHelper.urlField, fieldHelper.favoriteCountField })
+                    .ifPresent(doc -> {
+                        final String favoriteUrl = doc == null ? null : DocumentUtil.getValue(doc, fieldHelper.urlField, String.class);
+                        final String userCode = userInfoHelper.getUserCode();
 
-            body = "\"result\":\"ok\"";
+                        if (StringUtil.isBlank(userCode)) {
+                            throw new WebApiException(2, "No user session.");
+                        } else if (StringUtil.isBlank(favoriteUrl)) {
+                            throw new WebApiException(2, "URL is null.");
+                        }
+
+                        boolean found = false;
+                        for (final String id : docIds) {
+                            if (docId.equals(id)) {
+                                found = true;
+                                break;
+                            }
+                        }
+                        if (!found) {
+                            throw new WebApiException(5, "Not found: " + favoriteUrl);
+                        }
+
+                        if (!favoriteLogService.addUrl(userCode, favoriteUrl)) {
+                            throw new WebApiException(4, "Failed to add url: " + favoriteUrl);
+                        }
+
+                        final String id = DocumentUtil.getValue(doc, fieldHelper.idField, String.class);
+                        final Long count = DocumentUtil.getValue(doc, fieldHelper.favoriteCountField, Long.class);
+                        if (count != null) {
+                            searchService.update(id, fieldHelper.favoriteCountField, count.longValue() + 1);
+                        } else {
+                            throw new WebApiException(7, "Failed to update count: " + favoriteUrl);
+                        }
+
+                        writeJsonResponse(0, "\"result\":\"ok\"", null);
+
+                    }).orElse(() -> {
+                        throw new WebApiException(6, "Not found: " + docId);
+                    });
 
         } catch (final Exception e) {
+            int status;
             if (e instanceof WebApiException) {
                 status = ((WebApiException) e).getStatusCode();
             } else {
                 status = 1;
             }
-            errMsg = e.getMessage();
+            writeJsonResponse(status, null, e.getMessage());
             if (logger.isDebugEnabled()) {
                 logger.debug("Failed to process a favorite request.", e);
             }
         }
 
-        writeJsonResponse(status, body, errMsg);
     }
 
     protected void processFavoritesRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
+        if (Constants.FALSE.equals(crawlerProperties.getProperty(Constants.USER_FAVORITE_PROPERTY, Constants.FALSE))) {
+            writeJsonResponse(9, null, "Unsupported operation.");
+            return;
+        }
+
+        final UserInfoHelper userInfoHelper = ComponentUtil.getUserInfoHelper();
+        final FieldHelper fieldHelper = ComponentUtil.getFieldHelper();
+        final SearchService searchService = ComponentUtil.getComponent(SearchService.class);
+        final FavoriteLogService favoriteLogService = ComponentUtil.getComponent(FavoriteLogService.class);
+
         int status = 0;
         String body = null;
         String errMsg = null;
 
         try {
-            chain.doFilter(new WebApiRequest(request, FAVORITES_API), new WebApiResponse(response));
-            WebApiUtil.validate();
-            final List<String> docIdList = WebApiUtil.getObject("docIdList");
+            final String queryId = request.getParameter("queryId");
+            final String userCode = userInfoHelper.getUserCode();
+
+            if (StringUtil.isBlank(userCode)) {
+                throw new WebApiException(2, "No user session.");
+            } else if (StringUtil.isBlank(queryId)) {
+                throw new WebApiException(3, "Query ID is null.");
+            }
+
+            final String[] docIds = userInfoHelper.getResultDocIds(queryId);
+            final List<Map<String, Object>> docList =
+                    searchService.getDocumentListByDocIds(docIds, new String[] { fieldHelper.urlField, fieldHelper.docIdField,
+                            fieldHelper.favoriteCountField });
+            List<String> urlList = new ArrayList<>(docList.size());
+            for (final Map<String, Object> doc : docList) {
+                final String urlObj = DocumentUtil.getValue(doc, fieldHelper.urlField, String.class);
+                if (urlObj != null) {
+                    urlList.add(urlObj.toString());
+                }
+            }
+            urlList = favoriteLogService.getUrlList(userCode, urlList);
+            final List<String> docIdList = new ArrayList<>(urlList.size());
+            for (final Map<String, Object> doc : docList) {
+                final String urlObj = DocumentUtil.getValue(doc, fieldHelper.urlField, String.class);
+                if (urlObj != null && urlList.contains(urlObj)) {
+                    final String docIdObj = DocumentUtil.getValue(doc, fieldHelper.docIdField, String.class);
+                    if (docIdObj != null) {
+                        docIdList.add(docIdObj);
+                    }
+                }
+            }
 
             final StringBuilder buf = new StringBuilder();
             buf.append("\"num\":").append(docIdList.size());
@@ -671,4 +658,103 @@ public class JsonApiManager extends BaseApiManager {
         return Integer.toHexString(ch).toUpperCase();
     }
 
+    protected static class SearchApiRequestParams implements SearchRequestParams {
+
+        private final HttpServletRequest request;
+
+        private int startPosition = -1;
+
+        private int pageSize = -1;
+
+        protected SearchApiRequestParams(final HttpServletRequest request) {
+            this.request = request;
+        }
+
+        @Override
+        public String getQuery() {
+            return request.getParameter("query");
+        }
+
+        @Override
+        public String getOperator() {
+            return request.getParameter("op");
+        }
+
+        @Override
+        public String[] getAdditional() {
+            return request.getParameterValues("additional");
+        }
+
+        @Override
+        public Map<String, String[]> getFields() {
+            // TODO Auto-generated method stub
+            return new HashMap<>();
+        }
+
+        @Override
+        public String[] getLanguages() {
+            return request.getParameterValues("lang");
+        }
+
+        @Override
+        public GeoInfo getGeoInfo() {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        @Override
+        public FacetInfo getFacetInfo() {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        @Override
+        public String getSort() {
+            return request.getParameter("sort");
+        }
+
+        @Override
+        public int getStartPosition() {
+            if (startPosition != -1) {
+                return startPosition;
+            }
+
+            final String start = request.getParameter("start");
+            final QueryHelper queryHelper = ComponentUtil.getQueryHelper();
+            if (StringUtil.isBlank(start)) {
+                startPosition = queryHelper.getDefaultStart();
+            } else {
+                try {
+                    startPosition = Integer.parseInt(start);
+                } catch (final NumberFormatException e) {
+                    startPosition = queryHelper.getDefaultStart();
+                }
+            }
+            return startPosition;
+        }
+
+        @Override
+        public int getPageSize() {
+            if (pageSize != -1) {
+                return pageSize;
+            }
+
+            final String num = request.getParameter("num");
+            final QueryHelper queryHelper = ComponentUtil.getQueryHelper();
+            if (StringUtil.isBlank(num)) {
+                pageSize = queryHelper.getDefaultPageSize();
+            } else {
+                try {
+                    pageSize = Integer.parseInt(num);
+                    if (pageSize > queryHelper.getMaxPageSize() || pageSize <= 0) {
+                        pageSize = queryHelper.getMaxPageSize();
+                    }
+                } catch (final NumberFormatException e) {
+                    pageSize = queryHelper.getDefaultPageSize();
+                }
+            }
+            return pageSize;
+        }
+
+    }
 }
diff --git a/src/main/java/org/codelibs/fess/api/xml/XmlApiManager.java b/src/main/java/org/codelibs/fess/api/xml/XmlApiManager.java
deleted file mode 100644
index 3a3518dc8946d1fef3b688876a35eaed34567da3..0000000000000000000000000000000000000000
--- a/src/main/java/org/codelibs/fess/api/xml/XmlApiManager.java
+++ /dev/null
@@ -1,407 +0,0 @@
-/*
- * Copyright 2009-2015 the CodeLibs Project and the Others.
- *
- * Licensed 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.
- */
-
-package org.codelibs.fess.api.xml;
-
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.commons.lang3.StringEscapeUtils;
-import org.codelibs.core.CoreLibConstants;
-import org.codelibs.core.lang.StringUtil;
-import org.codelibs.fess.Constants;
-import org.codelibs.fess.api.BaseApiManager;
-import org.codelibs.fess.api.WebApiRequest;
-import org.codelibs.fess.api.WebApiResponse;
-import org.codelibs.fess.entity.PingResponse;
-import org.codelibs.fess.es.client.FessEsClient;
-import org.codelibs.fess.util.ComponentUtil;
-import org.codelibs.fess.util.FacetResponse;
-import org.codelibs.fess.util.FacetResponse.Field;
-import org.codelibs.fess.util.MoreLikeThisResponse;
-import org.codelibs.fess.util.WebApiUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class XmlApiManager extends BaseApiManager {
-    private static final Logger logger = LoggerFactory.getLogger(XmlApiManager.class);
-
-    public XmlApiManager() {
-        setPathPrefix("/xml");
-    }
-
-    @Override
-    public boolean matches(final HttpServletRequest request) {
-        if (Constants.FALSE.equals(ComponentUtil.getCrawlerProperties().getProperty(Constants.WEB_API_XML_PROPERTY, Constants.TRUE))) {
-            return false;
-        }
-
-        final String servletPath = request.getServletPath();
-        return servletPath.startsWith(pathPrefix);
-    }
-
-    @Override
-    public void process(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException,
-            ServletException {
-        final String formatType = request.getParameter("type");
-        switch (getFormatType(formatType)) {
-        case SEARCH:
-            processSearchRequest(request, response, chain);
-            break;
-        case LABEL:
-            processLabelRequest(request, response, chain);
-            break;
-        case SUGGEST:
-            processSuggestRequest(request, response, chain);
-            break;
-        case PING:
-            processPingRequest(request, response, chain);
-            break;
-        default:
-            writeXmlResponse(-1, StringUtil.EMPTY, "Not found.");
-            break;
-        }
-
-    }
-
-    protected void processPingRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
-        final FessEsClient fessEsClient = ComponentUtil.getElasticsearchClient();
-        int status;
-        String errMsg = null;
-        try {
-            final PingResponse pingResponse = fessEsClient.ping();
-            status = pingResponse.getStatus();
-        } catch (final Exception e) {
-            status = 9;
-            errMsg = e.getMessage();
-            if (errMsg == null) {
-                errMsg = e.getClass().getName();
-            }
-            if (logger.isDebugEnabled()) {
-                logger.debug("Failed to process a ping request.", e);
-            }
-        }
-
-        writeXmlResponse(status, null, errMsg);
-    }
-
-    protected void processSearchRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
-        int status = 0;
-        String errMsg = StringUtil.EMPTY;
-        final StringBuilder buf = new StringBuilder(1000);
-        String query = null;
-        request.setAttribute(Constants.SEARCH_LOG_ACCESS_TYPE, Constants.SEARCH_LOG_ACCESS_TYPE_XML);
-        final String queryId = request.getParameter("queryId");
-        try {
-            chain.doFilter(new WebApiRequest(request, SEARCH_API), new WebApiResponse(response));
-            WebApiUtil.validate();
-            query = WebApiUtil.getObject("searchQuery");
-            final String execTime = WebApiUtil.getObject("execTime");
-            final String queryTime = WebApiUtil.getObject("queryTime");
-            final String searchTime = WebApiUtil.getObject("searchTime");
-            final String pageSize = WebApiUtil.getObject("pageSize");
-            final String currentPageNumber = WebApiUtil.getObject("currentPageNumber");
-            final String allRecordCount = WebApiUtil.getObject("allRecordCount");
-            final String allPageCount = WebApiUtil.getObject("allPageCount");
-            final List<Map<String, Object>> documentItems = WebApiUtil.getObject("documentItems");
-            final FacetResponse facetResponse = WebApiUtil.getObject("facetResponse");
-            final MoreLikeThisResponse moreLikeThisResponse = WebApiUtil.getObject("moreLikeThisResponse");
-
-            buf.append("<query>");
-            buf.append(escapeXml(query));
-            buf.append("</query>");
-            buf.append("<exec-time>");
-            buf.append(execTime);
-            buf.append("</exec-time>");
-            buf.append("<query-time>");
-            buf.append(queryTime);
-            buf.append("</query-time>");
-            buf.append("<search-time>");
-            buf.append(searchTime);
-            buf.append("</search-time>");
-            if (StringUtil.isNotBlank(queryId)) {
-                buf.append("<query-id>");
-                buf.append(escapeXml(queryId));
-                buf.append("</query-id>");
-            }
-            buf.append("<page-size>");
-            buf.append(pageSize);
-            buf.append("</page-size>");
-            buf.append("<page-number>");
-            buf.append(currentPageNumber);
-            buf.append("</page-number>");
-            buf.append("<record-count>");
-            buf.append(allRecordCount);
-            buf.append("</record-count>");
-            buf.append("<page-count>");
-            buf.append(allPageCount);
-            buf.append("</page-count>");
-            buf.append("<result>");
-            for (final Map<String, Object> document : documentItems) {
-                buf.append("<doc>");
-                for (final Map.Entry<String, Object> entry : document.entrySet()) {
-                    final String name = entry.getKey();
-                    if (StringUtil.isNotBlank(name) && entry.getValue() != null && ComponentUtil.getQueryHelper().isApiResponseField(name)) {
-                        final String tagName = convertTagName(name);
-                        buf.append('<');
-                        buf.append(tagName);
-                        buf.append('>');
-                        buf.append(escapeXml(entry.getValue()));
-                        buf.append("</");
-                        buf.append(tagName);
-                        buf.append('>');
-                    }
-                }
-                buf.append("</doc>");
-            }
-            buf.append("</result>");
-            if (facetResponse != null && facetResponse.hasFacetResponse()) {
-                buf.append("<facet>");
-                // facet field
-                if (facetResponse.getFieldList() != null) {
-                    for (final Field field : facetResponse.getFieldList()) {
-                        buf.append("<field name=\"");
-                        buf.append(escapeXml(field.getName()));
-                        buf.append("\">");
-                        for (final Map.Entry<String, Long> entry : field.getValueCountMap().entrySet()) {
-                            buf.append("<value count=\"");
-                            buf.append(escapeXml(entry.getValue()));
-                            buf.append("\">");
-                            buf.append(escapeXml(entry.getKey()));
-                            buf.append("</value>");
-                        }
-                        buf.append("</field>");
-                    }
-                }
-                // facet query
-                if (facetResponse.getQueryCountMap() != null) {
-                    buf.append("<query>");
-                    for (final Map.Entry<String, Long> entry : facetResponse.getQueryCountMap().entrySet()) {
-                        buf.append("<value count=\"");
-                        buf.append(escapeXml(entry.getValue()));
-                        buf.append("\">");
-                        buf.append(escapeXml(entry.getKey()));
-                        buf.append("</value>");
-                    }
-                    buf.append("</query>");
-                }
-                buf.append("</facet>");
-            }
-            if (moreLikeThisResponse != null && !moreLikeThisResponse.isEmpty()) {
-                buf.append("<more-like-this>");
-                for (final Map.Entry<String, List<Map<String, Object>>> mltEntry : moreLikeThisResponse.entrySet()) {
-                    buf.append("<result id=\"");
-                    buf.append(escapeXml(mltEntry.getKey()));
-                    buf.append("\">");
-                    for (final Map<String, Object> document : mltEntry.getValue()) {
-                        buf.append("<doc>");
-                        for (final Map.Entry<String, Object> entry : document.entrySet()) {
-                            if (StringUtil.isNotBlank(entry.getKey()) && entry.getValue() != null) {
-                                final String tagName = convertTagName(entry.getKey());
-                                buf.append('<');
-                                buf.append(tagName);
-                                buf.append('>');
-                                buf.append(escapeXml(entry.getValue().toString()));
-                                buf.append("</");
-                                buf.append(tagName);
-                                buf.append('>');
-                            }
-                        }
-                        buf.append("</doc>");
-                    }
-                    buf.append("</result>");
-                }
-                buf.append("</more-like-this>");
-            }
-        } catch (final Exception e) {
-            status = 1;
-            errMsg = e.getMessage();
-            if (errMsg == null) {
-                errMsg = e.getClass().getName();
-            }
-            if (logger.isDebugEnabled()) {
-                logger.debug("Failed to process a search request.", e);
-            }
-        }
-
-        writeXmlResponse(status, buf.toString(), errMsg);
-    }
-
-    private String convertTagName(final String name) {
-        final String tagName = StringUtil.decamelize(name).replaceAll("_", "-").toLowerCase();
-        return tagName;
-    }
-
-    protected void processLabelRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
-        int status = 0;
-        String errMsg = StringUtil.EMPTY;
-        final StringBuilder buf = new StringBuilder(255);
-        try {
-            final List<Map<String, String>> labelTypeItems = ComponentUtil.getLabelTypeHelper().getLabelTypeItemList();
-            buf.append("<record-count>");
-            buf.append(labelTypeItems.size());
-            buf.append("</record-count>");
-            buf.append("<result>");
-            for (final Map<String, String> labelMap : labelTypeItems) {
-                buf.append("<label>");
-                buf.append("<name>");
-                buf.append(escapeXml(labelMap.get(Constants.ITEM_LABEL)));
-                buf.append("</name>");
-                buf.append("<value>");
-                buf.append(escapeXml(labelMap.get(Constants.ITEM_VALUE)));
-                buf.append("</value>");
-                buf.append("</label>");
-            }
-            buf.append("</result>");
-        } catch (final Exception e) {
-            status = 1;
-            errMsg = e.getMessage();
-            if (logger.isDebugEnabled()) {
-                logger.debug("Failed to process a label request.", e);
-            }
-
-        }
-
-        writeXmlResponse(status, buf.toString(), errMsg);
-    }
-
-    protected void processSuggestRequest(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) {
-        // TODO
-        //        int status = 0;
-        //        String errMsg = StringUtil.EMPTY;
-        //        final StringBuilder buf = new StringBuilder(255);
-        //        try {
-        //            chain.doFilter(new WebApiRequest(request, SUGGEST_API), new WebApiResponse(response));
-        //            WebApiUtil.validate();
-        //            final Integer suggestRecordCount = WebApiUtil.getObject("suggestRecordCount");
-        //            final List<SuggestResponse> suggestResultList = WebApiUtil.getObject("suggestResultList");
-        //            final List<String> suggestFieldName = WebApiUtil.getObject("suggestFieldName");
-        //
-        //            buf.append("<record-count>");
-        //            buf.append(suggestRecordCount);
-        //            buf.append("</record-count>");
-        //            if (suggestResultList.size() > 0) {
-        //                buf.append("<result>");
-        //
-        //                for (int i = 0; i < suggestResultList.size(); i++) {
-        //
-        //                    final SuggestResponse suggestResponse = suggestResultList.get(i);
-        //
-        //                    for (final Map.Entry<String, List<String>> entry : suggestResponse.entrySet()) {
-        //                        final SuggestResponseList srList = (SuggestResponseList) entry.getValue();
-        //                        final String fn = suggestFieldName.get(i);
-        //                        buf.append("<suggest>");
-        //                        buf.append("<token>");
-        //                        buf.append(escapeXml(entry.getKey()));
-        //                        buf.append("</token>");
-        //                        buf.append("<fn>");
-        //                        buf.append(escapeXml(fn));
-        //                        buf.append("</fn>");
-        //                        buf.append("<start-offset>");
-        //                        buf.append(escapeXml(Integer.toString(srList.getStartOffset())));
-        //                        buf.append("</start-offset>");
-        //                        buf.append("<end-offset>");
-        //                        buf.append(escapeXml(Integer.toString(srList.getEndOffset())));
-        //                        buf.append("</end-offset>");
-        //                        buf.append("<num-found>");
-        //                        buf.append(escapeXml(Integer.toString(srList.getNumFound())));
-        //                        buf.append("</num-found>");
-        //                        buf.append("<result>");
-        //                        for (final String value : srList) {
-        //                            buf.append("<value>");
-        //                            buf.append(escapeXml(value));
-        //                            buf.append("</value>");
-        //                        }
-        //                        buf.append("</result>");
-        //                        buf.append("</suggest>");
-        //
-        //                    }
-        //                }
-        //                buf.append("</result>");
-        //            }
-        //        } catch (final Exception e) {
-        //            if (e instanceof WebApiException) {
-        //                status = ((WebApiException) e).getStatusCode();
-        //            } else {
-        //                status = 1;
-        //            }
-        //            errMsg = e.getMessage();
-        //            if (logger.isDebugEnabled()) {
-        //                logger.debug("Failed to process a suggest request.", e);
-        //            }
-        //        }
-        //
-        //        writeXmlResponse(status, buf.toString(), errMsg);
-    }
-
-    protected void writeXmlResponse(final int status, final String body, final String errMsg) {
-        final StringBuilder buf = new StringBuilder(1000);
-        buf.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
-        buf.append("<response>");
-        buf.append("<version>");
-        buf.append(Constants.WEB_API_VERSION);
-        buf.append("</version>");
-        buf.append("<status>");
-        buf.append(status);
-        buf.append("</status>");
-        if (status == 0) {
-            if (StringUtil.isNotBlank(body)) {
-                buf.append(body);
-            }
-        } else {
-            buf.append("<message>");
-            buf.append(escapeXml(errMsg));
-            buf.append("</message>");
-        }
-        buf.append("</response>");
-        write(buf.toString(), "text/xml", Constants.UTF_8);
-
-    }
-
-    protected String escapeXml(final Object obj) {
-        final StringBuilder buf = new StringBuilder(255);
-        if (obj instanceof List<?>) {
-            buf.append("<list>");
-            for (final Object child : (List<?>) obj) {
-                buf.append("<item>").append(escapeXml(child)).append("</item>");
-            }
-            buf.append("</list>");
-        } else if (obj instanceof Map<?, ?>) {
-            buf.append("<data>");
-            for (final Map.Entry<?, ?> entry : ((Map<?, ?>) obj).entrySet()) {
-
-                buf.append("<name>").append(escapeXml(entry.getKey())).append("</name><value>").append(escapeXml(entry.getValue()))
-                        .append("</value>");
-            }
-            buf.append("</data>");
-        } else if (obj instanceof Date) {
-            final SimpleDateFormat sdf = new SimpleDateFormat(CoreLibConstants.DATE_FORMAT_ISO_8601_EXTEND);
-            buf.append(StringEscapeUtils.escapeXml(sdf.format(obj)));
-        } else if (obj != null) {
-            buf.append(StringEscapeUtils.escapeXml(obj.toString()));
-        }
-        return buf.toString();
-    }
-
-}
diff --git a/src/main/java/org/codelibs/fess/app/service/FavoriteLogService.java b/src/main/java/org/codelibs/fess/app/service/FavoriteLogService.java
new file mode 100644
index 0000000000000000000000000000000000000000..e88597ae2e757e8bcd99cd8eb0e30d92bad1af32
--- /dev/null
+++ b/src/main/java/org/codelibs/fess/app/service/FavoriteLogService.java
@@ -0,0 +1,62 @@
+package org.codelibs.fess.app.service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Resource;
+
+import org.codelibs.fess.es.exbhv.FavoriteLogBhv;
+import org.codelibs.fess.es.exbhv.UserInfoBhv;
+import org.codelibs.fess.es.exentity.FavoriteLog;
+import org.codelibs.fess.helper.SystemHelper;
+import org.dbflute.cbean.result.ListResultBean;
+
+public class FavoriteLogService {
+    @Resource
+    protected SystemHelper systemHelper;
+
+    @Resource
+    protected UserInfoBhv userInfoBhv;
+
+    @Resource
+    protected FavoriteLogBhv favoriteLogBhv;
+
+    public boolean addUrl(final String userCode, final String url) {
+        return userInfoBhv.selectEntity(cb -> {
+            cb.query().setCode_Equal(userCode);
+        }).map(userInfo -> {
+            final FavoriteLog favoriteLog = new FavoriteLog();
+            favoriteLog.setUserInfoId(userInfo.getId());
+            favoriteLog.setUrl(url);
+            favoriteLog.setCreatedTime(systemHelper.getCurrentTimeAsLong());
+            favoriteLogBhv.insert(favoriteLog);
+            return true;
+        }).orElse(false);
+    }
+
+    public List<String> getUrlList(final String userCode, final List<String> urlList) {
+        if (urlList.isEmpty()) {
+            return urlList;
+        }
+
+        return userInfoBhv.selectEntity(cb -> {
+            cb.query().setCode_Equal(userCode);
+        }).map(userInfo -> {
+            final ListResultBean<FavoriteLog> list = favoriteLogBhv.selectList(cb2 -> {
+                cb2.query().setUserInfoId_Equal(userInfo.getId());
+                cb2.query().setUrl_InScope(urlList);
+            });
+            if (!list.isEmpty()) {
+                final List<String> newUrlList = new ArrayList<>(list.size());
+                for (final FavoriteLog favoriteLog : list) {
+                    newUrlList.add(favoriteLog.getUrl());
+                }
+                return newUrlList;
+            }
+            return Collections.<String> emptyList();
+        }).orElse(Collections.<String> emptyList());
+
+    }
+
+}
diff --git a/src/main/java/org/codelibs/fess/app/service/SearchService.java b/src/main/java/org/codelibs/fess/app/service/SearchService.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8a409c950d1e1aca7bdb1a9e07413ea255e45b8
--- /dev/null
+++ b/src/main/java/org/codelibs/fess/app/service/SearchService.java
@@ -0,0 +1,324 @@
+package org.codelibs.fess.app.service;
+
+import java.text.NumberFormat;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringUtils;
+import org.codelibs.core.lang.StringUtil;
+import org.codelibs.core.misc.DynamicProperties;
+import org.codelibs.fess.Constants;
+import org.codelibs.fess.entity.SearchRenderData;
+import org.codelibs.fess.entity.SearchRequestParams;
+import org.codelibs.fess.es.client.FessEsClient;
+import org.codelibs.fess.es.client.FessEsClient.SearchConditionBuilder;
+import org.codelibs.fess.es.exentity.SearchLog;
+import org.codelibs.fess.es.exentity.UserInfo;
+import org.codelibs.fess.helper.FieldHelper;
+import org.codelibs.fess.helper.QueryHelper;
+import org.codelibs.fess.helper.SearchLogHelper;
+import org.codelibs.fess.helper.SystemHelper;
+import org.codelibs.fess.helper.UserInfoHelper;
+import org.codelibs.fess.util.ComponentUtil;
+import org.codelibs.fess.util.QueryResponseList;
+import org.dbflute.optional.OptionalEntity;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SearchService {
+
+    // ===================================================================================
+    //                                                                            Constant
+    //
+    private static final Logger logger = LoggerFactory.getLogger(SearchService.class);
+
+    protected static final Pattern FIELD_EXTRACTION_PATTERN = Pattern.compile("^([a-zA-Z0-9_]+):.*");
+
+    // ===================================================================================
+    //                                                                           Attribute
+    //
+    @Resource
+    protected DynamicProperties crawlerProperties;
+
+    @Resource
+    protected FessEsClient fessEsClient;
+
+    @Resource
+    protected SystemHelper systemHelper;
+
+    @Resource
+    protected FieldHelper fieldHelper;
+
+    @Resource
+    protected QueryHelper queryHelper;
+
+    @Resource
+    protected UserInfoHelper userInfoHelper;
+
+    // ===================================================================================
+    //                                                                              Method
+    //                                                                      ==============
+
+    public void search(final HttpServletRequest request, final SearchRequestParams params, final SearchRenderData data) {
+        final long startTime = System.currentTimeMillis();
+        final boolean searchLogSupport =
+                Constants.TRUE.equals(crawlerProperties.getProperty(Constants.SEARCH_LOG_PROPERTY, Constants.TRUE));
+
+        if (StringUtil.isNotBlank(params.getOperator())) {
+            request.setAttribute(Constants.DEFAULT_OPERATOR, params.getOperator());
+        }
+
+        final StringBuilder queryBuf = new StringBuilder(255);
+        if (StringUtil.isNotBlank(params.getQuery())) {
+            if (params.getQuery().indexOf(" OR ") >= 0) {
+                queryBuf.append('(').append(params.getQuery()).append(')');
+            } else {
+                queryBuf.append(params.getQuery());
+            }
+        }
+        if (params.getAdditional() != null) {
+            appendAdditionalQuery(params.getAdditional(), additional -> {
+                queryBuf.append(' ').append(additional);
+            });
+        }
+        params.getFields().entrySet().stream().forEach(entry -> {
+            appendQueries(queryBuf, entry.getKey(), entry.getValue());
+        });
+        if (StringUtil.isNotBlank(params.getSort())) {
+            queryBuf.append(" sort:").append(params.getSort());
+        }
+        if (params.getLanguages() != null) {
+            appendQueries(queryBuf, fieldHelper.langField, params.getLanguages());
+        }
+
+        final String query = queryBuf.toString().trim();
+
+        final int pageStart = params.getStartPosition();
+        final int pageSize = params.getPageSize();
+        final List<Map<String, Object>> documentItems =
+                fessEsClient.search(
+                        fieldHelper.docIndex,
+                        fieldHelper.docType,
+                        searchRequestBuilder -> {
+                            return SearchConditionBuilder.builder(searchRequestBuilder).query(query).offset(pageStart).size(pageSize)
+                                    .facetInfo(params.getFacetInfo()).geoInfo(params.getGeoInfo())
+                                    .responseFields(queryHelper.getResponseFields()).build();
+                        }, (searchRequestBuilder, execTime, searchResponse) -> {
+                            final QueryResponseList queryResponseList = ComponentUtil.getQueryResponseList();
+                            queryResponseList.init(searchResponse, pageStart, pageSize);
+                            return queryResponseList;
+                        });
+        data.setDocumentItems(documentItems);
+
+        // search
+        final QueryResponseList queryResponseList = (QueryResponseList) documentItems;
+        data.setFacetResponse(queryResponseList.getFacetResponse());
+
+        final String[] highlightQueries = (String[]) request.getAttribute(Constants.HIGHLIGHT_QUERIES);
+        if (highlightQueries != null) {
+            final StringBuilder buf = new StringBuilder(100);
+            for (final String q : highlightQueries) {
+                buf.append("&hq=").append(q);
+            }
+            data.setAppendHighlightParams(buf.toString());
+        }
+
+        // search log
+        if (searchLogSupport) {
+            storeSearchLog(request, query, pageStart, pageSize, queryResponseList);
+        }
+
+        queryResponseList.setExecTime(System.currentTimeMillis() - startTime);
+        final NumberFormat nf = NumberFormat.getInstance(request.getLocale());
+        nf.setMaximumIntegerDigits(2);
+        nf.setMaximumFractionDigits(2);
+        String execTime;
+        try {
+            execTime = nf.format((double) queryResponseList.getExecTime() / 1000);
+        } catch (final Exception e) {
+            execTime = StringUtil.EMPTY;
+        }
+        data.setExecTime(execTime);
+
+        data.setPageSize(queryResponseList.getPageSize());
+        data.setCurrentPageNumber(queryResponseList.getCurrentPageNumber());
+        data.setAllRecordCount(queryResponseList.getAllRecordCount());
+        data.setAllPageCount(queryResponseList.getAllPageCount());
+        data.setExistNextPage(queryResponseList.isExistNextPage());
+        data.setExistPrevPage(queryResponseList.isExistPrevPage());
+        data.setCurrentStartRecordNumber(queryResponseList.getCurrentStartRecordNumber());
+        data.setCurrentEndRecordNumber(queryResponseList.getCurrentEndRecordNumber());
+        data.setPageNumberList(queryResponseList.getPageNumberList());
+        data.setPartialResults(queryResponseList.isPartialResults());
+        data.setQueryTime(queryResponseList.getQueryTime());
+        data.setSearchQuery(query);
+    }
+
+    protected void storeSearchLog(final HttpServletRequest request, final String query, final int pageStart, final int pageSize,
+            final QueryResponseList queryResponseList) {
+        final long now = systemHelper.getCurrentTimeAsLong();
+
+        final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
+        final SearchLog searchLog = new SearchLog();
+
+        String userCode = null;
+        if (Constants.TRUE.equals(crawlerProperties.getProperty(Constants.USER_INFO_PROPERTY, Constants.TRUE))) {
+            userCode = userInfoHelper.getUserCode();
+            if (StringUtil.isNotBlank(userCode)) {
+                final UserInfo userInfo = new UserInfo();
+                userInfo.setCode(userCode);
+                userInfo.setCreatedTime(now);
+                userInfo.setUpdatedTime(now);
+                searchLog.setUserInfo(OptionalEntity.of(userInfo));
+            }
+        }
+
+        searchLog.setHitCount(queryResponseList.getAllRecordCount());
+        searchLog.setResponseTime(Integer.valueOf((int) queryResponseList.getExecTime()));
+        searchLog.setSearchWord(StringUtils.abbreviate(query, 1000));
+        searchLog.setSearchQuery(StringUtils.abbreviate(queryResponseList.getSearchQuery(), 1000));
+        searchLog.setRequestedTime(now);
+        searchLog.setQueryOffset(pageStart);
+        searchLog.setQueryPageSize(pageSize);
+
+        searchLog.setClientIp(StringUtils.abbreviate(request.getRemoteAddr(), 50));
+        searchLog.setReferer(StringUtils.abbreviate(request.getHeader("referer"), 1000));
+        searchLog.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 255));
+        if (userCode != null) {
+            searchLog.setUserSessionId(userCode);
+        }
+        final Object accessType = request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE);
+        if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) {
+            searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
+        } else if (Constants.SEARCH_LOG_ACCESS_TYPE_XML.equals(accessType)) {
+            searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_XML);
+        } else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) {
+            searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER);
+        } else {
+            searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_WEB);
+        }
+
+        @SuppressWarnings("unchecked")
+        final Map<String, List<String>> fieldLogMap = (Map<String, List<String>>) request.getAttribute(Constants.FIELD_LOGS);
+        if (fieldLogMap != null) {
+            for (final Map.Entry<String, List<String>> logEntry : fieldLogMap.entrySet()) {
+                for (final String value : logEntry.getValue()) {
+                    searchLog.addSearchFieldLogValue(logEntry.getKey(), StringUtils.abbreviate(value, 1000));
+                }
+            }
+        }
+
+        searchLogHelper.addSearchLog(searchLog);
+    }
+
+    public String[] getLanguages(final HttpServletRequest request, final SearchRequestParams params) {
+        if (params.getLanguages() != null) {
+            final Set<String> langSet = new HashSet<>();
+            for (final String lang : params.getLanguages()) {
+                if (StringUtil.isNotBlank(lang) && lang.length() < 1000) {
+                    if (Constants.ALL_LANGUAGES.equalsIgnoreCase(lang)) {
+                        langSet.add(Constants.ALL_LANGUAGES);
+                    } else {
+                        final String normalizeLang = systemHelper.normalizeLang(lang);
+                        if (normalizeLang != null) {
+                            langSet.add(normalizeLang);
+                        }
+                    }
+                }
+            }
+            if (langSet.size() > 1 && langSet.contains(Constants.ALL_LANGUAGES)) {
+                return new String[] { Constants.ALL_LANGUAGES };
+            } else {
+                langSet.remove(Constants.ALL_LANGUAGES);
+            }
+            return langSet.toArray(new String[langSet.size()]);
+        } else if (Constants.TRUE.equals(crawlerProperties.getProperty(Constants.USE_BROWSER_LOCALE_FOR_SEARCH_PROPERTY, Constants.FALSE))) {
+            final Set<String> langSet = new HashSet<>();
+            final Enumeration<Locale> locales = request.getLocales();
+            if (locales != null) {
+                while (locales.hasMoreElements()) {
+                    final Locale locale = locales.nextElement();
+                    final String normalizeLang = systemHelper.normalizeLang(locale.toString());
+                    if (normalizeLang != null) {
+                        langSet.add(normalizeLang);
+                    }
+                }
+                if (!langSet.isEmpty()) {
+                    return langSet.toArray(new String[langSet.size()]);
+                }
+            }
+        }
+        return StringUtil.EMPTY_STRINGS;
+    }
+
+    protected void appendQueries(final StringBuilder queryBuf, final String key, final String[] values) {
+        if (values.length == 1) {
+            queryBuf.append(' ').append(key).append(":\"").append(values[0]).append('\"');
+        } else if (values.length > 1) {
+            boolean first = true;
+            queryBuf.append(" (");
+            for (final String value : values) {
+                if (first) {
+                    first = false;
+                } else {
+                    queryBuf.append(" OR ");
+                }
+                queryBuf.append(key).append(":\"").append(value).append('\"');
+            }
+            queryBuf.append(')');
+        }
+    }
+
+    public void appendAdditionalQuery(final String[] additionalQueries, final Consumer<String> consumer) {
+        final Set<String> fieldSet = new HashSet<>();
+        for (final String additional : additionalQueries) {
+            if (StringUtil.isNotBlank(additional) && additional.length() < 1000 && !hasFieldInQuery(fieldSet, additional)) {
+                consumer.accept(additional);
+            }
+        }
+    }
+
+    protected boolean hasFieldInQuery(final Set<String> fieldSet, final String query) {
+        final Matcher matcher = FIELD_EXTRACTION_PATTERN.matcher(query);
+        if (matcher.matches()) {
+            final String field = matcher.replaceFirst("$1");
+            if (fieldSet.contains(field)) {
+                return true;
+            }
+            fieldSet.add(field);
+        }
+        return false;
+    }
+
+    public OptionalEntity<Map<String, Object>> getDocumentByDocId(final String docId, final String[] fields) {
+        return fessEsClient.getDocument(fieldHelper.docIndex, fieldHelper.docType, builder -> {
+            builder.setQuery(QueryBuilders.termQuery(fieldHelper.docIdField, docId));
+            builder.addFields(fields);
+            return true;
+        });
+    }
+
+    public List<Map<String, Object>> getDocumentListByDocIds(final String[] docIds, final String[] fields) {
+        return fessEsClient.getDocumentList(fieldHelper.docIndex, fieldHelper.docType, builder -> {
+            builder.setQuery(QueryBuilders.termsQuery(fieldHelper.docIdField, docIds));
+            builder.setSize(queryHelper.getMaxPageSize());
+            builder.addFields(fields);
+            return true;
+        });
+    }
+
+    public boolean update(final String id, final String field, final Object value) {
+        return fessEsClient.update(fieldHelper.docIndex, fieldHelper.docType, id, field, value);
+    }
+}
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/dataconfig/AdminDataconfigAction.java b/src/main/java/org/codelibs/fess/app/web/admin/dataconfig/AdminDataconfigAction.java
index 2ae39edc3524eb2c4390d53af19bf40ee6077dd7..b6409985e9da5504a866cb85aa2661165d9489c9 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/dataconfig/AdminDataconfigAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/dataconfig/AdminDataconfigAction.java
@@ -30,9 +30,6 @@ import org.codelibs.fess.app.service.DataConfigService;
 import org.codelibs.fess.app.service.LabelTypeService;
 import org.codelibs.fess.app.service.RoleTypeService;
 import org.codelibs.fess.app.web.CrudMode;
-import org.codelibs.fess.app.web.admin.dataconfig.CreateForm;
-import org.codelibs.fess.app.web.admin.dataconfig.EditForm;
-import org.codelibs.fess.app.web.admin.dataconfig.SearchForm;
 import org.codelibs.fess.app.web.base.FessAdminAction;
 import org.codelibs.fess.ds.DataStoreFactory;
 import org.codelibs.fess.es.exentity.DataConfig;
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/dict/AdminDictAction.java b/src/main/java/org/codelibs/fess/app/web/admin/dict/AdminDictAction.java
index 0292fb53929b73d1cb852da968e730869771dae5..a06a9fe520d6cb6751015f097f8b9656e7f8b452 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/dict/AdminDictAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/dict/AdminDictAction.java
@@ -55,7 +55,7 @@ public class AdminDictAction extends FessAdminAction {
     @Execute
     public HtmlResponse index(final ListForm form) {
         return asHtml(path_AdminDict_IndexJsp).renderWith(data -> {
-            DictionaryFile<? extends DictionaryItem>[] dictFiles = dictionaryManager.getDictionaryFiles();
+            final DictionaryFile<? extends DictionaryItem>[] dictFiles = dictionaryManager.getDictionaryFiles();
             data.register("dictFiles", dictFiles);
         });
     }
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/dict/kuromoji/AdminDictKuromojiAction.java b/src/main/java/org/codelibs/fess/app/web/admin/dict/kuromoji/AdminDictKuromojiAction.java
index 24c42d8112829610a11a6e4ce6ade232b66232ad..85c96813895e96396d23c8db7d2e1aec75702839 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/dict/kuromoji/AdminDictKuromojiAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/dict/kuromoji/AdminDictKuromojiAction.java
@@ -311,7 +311,7 @@ public class AdminDictKuromojiAction extends FessAdminAction {
         return kuromojiService.getKuromojiFile(form.dictId).map(file -> {
             try (InputStream inputStream = form.kuromojiFile.getInputStream()) {
                 file.update(inputStream);
-            } catch (IOException e) {
+            } catch (final IOException e) {
                 throwValidationError(messages -> messages.addErrorsFailedToUploadKuromojiFile(GLOBAL), () -> {
                     return redirectWith(getClass(), moreUrl("uploadpage/" + form.dictId));
                 });
@@ -381,7 +381,7 @@ public class AdminDictKuromojiAction extends FessAdminAction {
     //                                                                        Assist Logic
     //                                                                        ============
 
-    protected OptionalEntity<KuromojiItem> createKuromojiItem(CreateForm form) {
+    protected OptionalEntity<KuromojiItem> createKuromojiItem(final CreateForm form) {
         switch (form.crudMode) {
         case CrudMode.CREATE:
             if (form instanceof CreateForm) {
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/dict/synonym/AdminDictSynonymAction.java b/src/main/java/org/codelibs/fess/app/web/admin/dict/synonym/AdminDictSynonymAction.java
index 9a2ebf4b881ef952e7bda76509763819ef21a2d0..c61559d9fe197ff25d7019e6d5d942845d0530b3 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/dict/synonym/AdminDictSynonymAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/dict/synonym/AdminDictSynonymAction.java
@@ -323,7 +323,7 @@ public class AdminDictSynonymAction extends FessAdminAction {
         return synonymService.getSynonymFile(form.dictId).map(file -> {
             try (InputStream inputStream = form.synonymFile.getInputStream()) {
                 file.update(inputStream);
-            } catch (IOException e) {
+            } catch (final IOException e) {
                 throwValidationError(messages -> messages.addErrorsFailedToUploadSynonymFile(GLOBAL), () -> {
                     return redirectWith(getClass(), moreUrl("uploadpage/" + form.dictId));
                 });
@@ -397,7 +397,7 @@ public class AdminDictSynonymAction extends FessAdminAction {
     //                                                                        Assist Logic
     //                                                                        ============
 
-    protected OptionalEntity<SynonymItem> createSynonymItem(CreateForm form) {
+    protected OptionalEntity<SynonymItem> createSynonymItem(final CreateForm form) {
         switch (form.crudMode) {
         case CrudMode.CREATE:
             if (form instanceof CreateForm) {
@@ -439,11 +439,11 @@ public class AdminDictSynonymAction extends FessAdminAction {
         };
     }
 
-    private void validateSynonymString(String[] values, VaErrorHook hook) {
+    private void validateSynonymString(final String[] values, final VaErrorHook hook) {
         if (values.length == 0) {
             return;
         }
-        for (String value : values) {
+        for (final String value : values) {
             if (value.indexOf(',') >= 0) {
                 throwValidationError(messages -> {
                     messages.addErrorsInvalidStrIsIncluded(GLOBAL, value, ",");
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/fileconfig/AdminFileconfigAction.java b/src/main/java/org/codelibs/fess/app/web/admin/fileconfig/AdminFileconfigAction.java
index 8b83f685ae99833e2bb538d73afbf18024705a55..f1785a7e9e140c18825910cf49b258c65dd6d607 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/fileconfig/AdminFileconfigAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/fileconfig/AdminFileconfigAction.java
@@ -25,9 +25,6 @@ import org.codelibs.fess.app.service.FileConfigService;
 import org.codelibs.fess.app.service.LabelTypeService;
 import org.codelibs.fess.app.service.RoleTypeService;
 import org.codelibs.fess.app.web.CrudMode;
-import org.codelibs.fess.app.web.admin.fileconfig.CreateForm;
-import org.codelibs.fess.app.web.admin.fileconfig.EditForm;
-import org.codelibs.fess.app.web.admin.fileconfig.SearchForm;
 import org.codelibs.fess.app.web.base.FessAdminAction;
 import org.codelibs.fess.es.exentity.FileConfig;
 import org.codelibs.fess.helper.SystemHelper;
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/general/AdminGeneralAction.java b/src/main/java/org/codelibs/fess/app/web/admin/general/AdminGeneralAction.java
index 77019277cab3038fe4b131b34827464808406aa6..ce026587cecf90c7b4762da276667325692a225a 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/general/AdminGeneralAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/general/AdminGeneralAction.java
@@ -148,12 +148,12 @@ public class AdminGeneralAction extends FessAdminAction {
         form.esHttpUrl = crawlerProperties.getProperty(Constants.ELASTICSEARCH_WEB_URL_PROPERTY, Constants.ELASTICSEARCH_WEB_URL);
     }
 
-    private Integer getPropertyAsInteger(String key, int defaultValue) {
-        String value = crawlerProperties.getProperty(Constants.CRAWLING_THREAD_COUNT_PROPERTY);
+    private Integer getPropertyAsInteger(final String key, final int defaultValue) {
+        final String value = crawlerProperties.getProperty(Constants.CRAWLING_THREAD_COUNT_PROPERTY);
         if (value != null) {
             try {
                 return Integer.valueOf(value);
-            } catch (NumberFormatException e) {
+            } catch (final NumberFormatException e) {
                 // ignore
             }
         }
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/keymatch/AdminKeymatchAction.java b/src/main/java/org/codelibs/fess/app/web/admin/keymatch/AdminKeymatchAction.java
index 1f5bcf466a7591f8fbc5ff5dbee1da48d414a0c7..a866e40fe74acc441b870d56c8fd56955991ee6a 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/keymatch/AdminKeymatchAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/keymatch/AdminKeymatchAction.java
@@ -163,7 +163,7 @@ public class AdminKeymatchAction extends FessAdminAction {
     public HtmlResponse editfromconfirm(final EditForm form) {
         validate(form, messages -> {}, toEditHtml());
         form.crudMode = CrudMode.EDIT;
-        String id = form.id;
+        final String id = form.id;
         keyMatchService.getKeyMatch(id).ifPresent(entity -> {
             copyBeanToBean(entity, form, op -> {});
         }).orElse(() -> {
@@ -195,7 +195,7 @@ public class AdminKeymatchAction extends FessAdminAction {
     public HtmlResponse deletefromconfirm(final EditForm form) {
         form.crudMode = CrudMode.DELETE;
         validate(form, messages -> {}, toEditHtml());
-        String id = form.id;
+        final String id = form.id;
         keyMatchService.getKeyMatch(id).ifPresent(entity -> {
             copyBeanToBean(entity, form, op -> {});
         }).orElse(() -> {
@@ -279,7 +279,7 @@ public class AdminKeymatchAction extends FessAdminAction {
     public HtmlResponse delete(final EditForm form) {
         verifyCrudMode(form.crudMode, CrudMode.DELETE);
         validate(form, messages -> {}, toEditHtml());
-        String id = form.id;
+        final String id = form.id;
         keyMatchService.getKeyMatch(id).ifPresent(entity -> {
             keyMatchService.delete(entity);
             saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
@@ -300,7 +300,7 @@ public class AdminKeymatchAction extends FessAdminAction {
         switch (form.crudMode) {
         case CrudMode.CREATE:
             if (form instanceof CreateForm) {
-                KeyMatch entity = new KeyMatch();
+                final KeyMatch entity = new KeyMatch();
                 entity.setCreatedBy(username);
                 entity.setCreatedTime(currentTime);
                 entity.setUpdatedBy(username);
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/labeltype/AdminLabeltypeAction.java b/src/main/java/org/codelibs/fess/app/web/admin/labeltype/AdminLabeltypeAction.java
index 7b8642dbdcf6c04e03e114ad58395abdf63a1e30..f76cb1643b27019a216bae9a0c6f28c222261ed6 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/labeltype/AdminLabeltypeAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/labeltype/AdminLabeltypeAction.java
@@ -24,9 +24,6 @@ import org.codelibs.fess.app.pager.LabelTypePager;
 import org.codelibs.fess.app.service.LabelTypeService;
 import org.codelibs.fess.app.service.RoleTypeService;
 import org.codelibs.fess.app.web.CrudMode;
-import org.codelibs.fess.app.web.admin.labeltype.CreateForm;
-import org.codelibs.fess.app.web.admin.labeltype.EditForm;
-import org.codelibs.fess.app.web.admin.labeltype.SearchForm;
 import org.codelibs.fess.app.web.base.FessAdminAction;
 import org.codelibs.fess.es.exentity.LabelType;
 import org.codelibs.fess.helper.SystemHelper;
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/requestheader/AdminRequestheaderAction.java b/src/main/java/org/codelibs/fess/app/web/admin/requestheader/AdminRequestheaderAction.java
index b20cb2f8026335b44fdc3ef00d27b18414e3efdc..e4c28a275490efca8dc66b36ccc3e35207425fc6 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/requestheader/AdminRequestheaderAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/requestheader/AdminRequestheaderAction.java
@@ -30,9 +30,6 @@ import org.codelibs.fess.app.pager.RequestHeaderPager;
 import org.codelibs.fess.app.service.RequestHeaderService;
 import org.codelibs.fess.app.service.WebConfigService;
 import org.codelibs.fess.app.web.CrudMode;
-import org.codelibs.fess.app.web.admin.requestheader.CreateForm;
-import org.codelibs.fess.app.web.admin.requestheader.EditForm;
-import org.codelibs.fess.app.web.admin.requestheader.SearchForm;
 import org.codelibs.fess.app.web.base.FessAdminAction;
 import org.codelibs.fess.es.exentity.RequestHeader;
 import org.codelibs.fess.es.exentity.WebConfig;
diff --git a/src/main/java/org/codelibs/fess/app/web/admin/webconfig/AdminWebconfigAction.java b/src/main/java/org/codelibs/fess/app/web/admin/webconfig/AdminWebconfigAction.java
index c575d0b08957c2d0ea553cf6cfc58c469297ca2f..74c00ce214159798624150acf7e0536eaa33470c 100644
--- a/src/main/java/org/codelibs/fess/app/web/admin/webconfig/AdminWebconfigAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/admin/webconfig/AdminWebconfigAction.java
@@ -21,13 +21,10 @@ import javax.annotation.Resource;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.annotation.Token;
 import org.codelibs.fess.app.pager.WebConfigPager;
-import org.codelibs.fess.app.service.WebConfigService;
 import org.codelibs.fess.app.service.LabelTypeService;
 import org.codelibs.fess.app.service.RoleTypeService;
+import org.codelibs.fess.app.service.WebConfigService;
 import org.codelibs.fess.app.web.CrudMode;
-import org.codelibs.fess.app.web.admin.webconfig.CreateForm;
-import org.codelibs.fess.app.web.admin.webconfig.EditForm;
-import org.codelibs.fess.app.web.admin.webconfig.SearchForm;
 import org.codelibs.fess.app.web.base.FessAdminAction;
 import org.codelibs.fess.es.exentity.WebConfig;
 import org.codelibs.fess.helper.SystemHelper;
diff --git a/src/main/java/org/codelibs/fess/app/web/go/GoForm.java b/src/main/java/org/codelibs/fess/app/web/go/GoForm.java
index a1925d237cd65d44b2c330599ca5334a19d6956d..da1426e42dd02fb500f62b3056647cee63b9efb9 100644
--- a/src/main/java/org/codelibs/fess/app/web/go/GoForm.java
+++ b/src/main/java/org/codelibs/fess/app/web/go/GoForm.java
@@ -1,8 +1,6 @@
 package org.codelibs.fess.app.web.go;
 
 public class GoForm {
-    private static final long serialVersionUID = 1L;
-
     //@Required(target = "go,cache")
     //@Maxbytelength(maxbytelength = 100)
     public String docId;
diff --git a/src/main/java/org/codelibs/fess/app/web/search/SearchAction.java b/src/main/java/org/codelibs/fess/app/web/search/SearchAction.java
index 054da603535c737c25c4b8042fe137fd826905f4..d6c9675783e49af79c68eb3f57baf662bcfef292 100644
--- a/src/main/java/org/codelibs/fess/app/web/search/SearchAction.java
+++ b/src/main/java/org/codelibs/fess/app/web/search/SearchAction.java
@@ -16,34 +16,24 @@
 
 package org.codelibs.fess.app.web.search;
 
-import java.text.NumberFormat;
-import java.time.Clock;
 import java.util.ArrayList;
-import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
+import javax.annotation.Resource;
 import javax.servlet.http.HttpSession;
 
-import org.apache.commons.lang3.StringUtils;
 import org.codelibs.core.lang.StringUtil;
 import org.codelibs.fess.Constants;
+import org.codelibs.fess.app.service.SearchService;
 import org.codelibs.fess.app.web.RootAction;
 import org.codelibs.fess.app.web.base.FessSearchAction;
-import org.codelibs.fess.es.client.FessEsClient.SearchConditionBuilder;
-import org.codelibs.fess.es.exentity.SearchLog;
-import org.codelibs.fess.es.exentity.UserInfo;
+import org.codelibs.fess.entity.SearchRenderData;
 import org.codelibs.fess.exception.InvalidQueryException;
 import org.codelibs.fess.exception.ResultOffsetExceededException;
-import org.codelibs.fess.helper.SearchLogHelper;
-import org.codelibs.fess.util.ComponentUtil;
-import org.codelibs.fess.util.QueryResponseList;
-import org.dbflute.optional.OptionalEntity;
+import org.codelibs.fess.util.FacetResponse;
 import org.lastaflute.taglib.function.LaFunctions;
 import org.lastaflute.web.Execute;
 import org.lastaflute.web.response.HtmlResponse;
@@ -59,17 +49,11 @@ public class SearchAction extends FessSearchAction {
     //
     private static final Logger logger = LoggerFactory.getLogger(SearchAction.class);
 
-    protected static final long DEFAULT_START_COUNT = 0;
-
-    protected static final int MAX_PAGE_SIZE = 100;
-
-    private static final int DEFAULT_PAGE_SIZE = 20;
-
-    protected static final Pattern FIELD_EXTRACTION_PATTERN = Pattern.compile("^([a-zA-Z0-9_]+):.*");
-
     // ===================================================================================
     //                                                                           Attribute
     //
+    @Resource
+    protected SearchService searchService;
 
     // ===================================================================================
     //                                                                               Hook
@@ -85,15 +69,16 @@ public class SearchAction extends FessSearchAction {
 
     @Execute
     public HtmlResponse search(final SearchForm form) {
-        if (viewHelper.isUseSession() && StringUtil.isNotBlank(form.num)) {
-            normalizePageNum(form);
-            final HttpSession session = request.getSession();
-            if (session != null) {
-                session.setAttribute(Constants.RESULTS_PER_PAGE, form.num);
-            }
+        final HtmlResponse response = doSearch(form);
+        if (viewHelper.isUseSession()) {
+            LaRequestUtil.getOptionalRequest().ifPresent(request -> {
+                final HttpSession session = request.getSession(false);
+                if (session != null) {
+                    session.setAttribute(Constants.RESULTS_PER_PAGE, form.num);
+                }
+            });
         }
-
-        return doSearch(form);
+        return response;
     }
 
     @Execute
@@ -145,297 +130,58 @@ public class SearchAction extends FessSearchAction {
         return asHtml(path_SearchJsp).renderWith(data -> {
             updateSearchParams(form);
             buildLabelParams(form.fields);
-            doSearchInternal(data, form);
-            data.register("displayQuery", getDisplayQuery(form, labelTypeHelper.getLabelTypeItemList()));
-            data.register("pagingQuery", getPagingQuery(form));
-        });
-    }
-
-    protected HtmlResponse doMove(final SearchForm form, final int move) {
-        int pageNum = getDefaultPageSize();
-        if (StringUtil.isBlank(form.num)) {
-            form.num = String.valueOf(getDefaultPageSize());
-        } else {
+            form.lang = searchService.getLanguages(request, form);
             try {
-                pageNum = Integer.parseInt(form.num);
-            } catch (final NumberFormatException e) {
-                form.num = String.valueOf(getDefaultPageSize());
-            }
-        }
-
-        if (StringUtil.isBlank(form.pn)) {
-            form.start = String.valueOf(DEFAULT_START_COUNT);
-        } else {
-            Integer pageNumber = Integer.parseInt(form.pn);
-            if (pageNumber != null && pageNumber > 0) {
-                pageNumber = pageNumber + move;
-                if (pageNumber < 1) {
-                    pageNumber = 1;
-                }
-                form.start = String.valueOf((pageNumber - 1) * pageNum);
-            } else {
-                form.start = String.valueOf(DEFAULT_START_COUNT);
-            }
-        }
-
-        return doSearch(form);
-    }
-
-    protected String doSearchInternal(final RenderData data, final SearchForm form) {
-        final StringBuilder queryBuf = new StringBuilder(255);
-        if (StringUtil.isNotBlank(form.query)) {
-            queryBuf.append(form.query);
-        }
-        if (StringUtil.isNotBlank(form.op)) {
-            request.setAttribute(Constants.DEFAULT_OPERATOR, form.op);
-        }
-        if (queryBuf.indexOf(" OR ") >= 0) {
-            queryBuf.insert(0, '(').append(')');
-        }
-        if (form.additional != null) {
-            final Set<String> fieldSet = new HashSet<String>();
-            for (final String additional : form.additional) {
-                if (StringUtil.isNotBlank(additional) && additional.length() < 1000 && !hasFieldInQuery(fieldSet, additional)) {
-                    queryBuf.append(' ').append(additional);
-                }
-            }
-        }
-        if (!form.fields.isEmpty()) {
-            for (final Map.Entry<String, String[]> entry : form.fields.entrySet()) {
-                final List<String> valueList = new ArrayList<String>();
-                final String[] values = entry.getValue();
-                if (values != null) {
-                    for (final String v : values) {
-                        valueList.add(v);
+                final WebRenderData renderData = new WebRenderData(data);
+                searchService.search(request, form, renderData);
+                // favorite or screenshot
+                if (favoriteSupport || screenShotManager != null) {
+                    final String searchQuery = renderData.getSearchQuery();
+                    final List<Map<String, Object>> documentItems = renderData.getDocumentItems();
+                    form.queryId = userInfoHelper.generateQueryId(searchQuery, documentItems);
+                    if (screenShotManager != null) {
+                        screenShotManager.storeRequest(form.queryId, documentItems);
+                        data.register("screenShotSupport", true);
                     }
                 }
-                if (valueList.size() == 1) {
-                    queryBuf.append(' ').append(entry.getKey()).append(":\"").append(valueList.get(0)).append('\"');
-                } else if (valueList.size() > 1) {
-                    queryBuf.append(" (");
-                    for (int i = 0; i < valueList.size(); i++) {
-                        if (i != 0) {
-                            queryBuf.append(" OR");
-                        }
-                        queryBuf.append(' ').append(entry.getKey()).append(":\"").append(valueList.get(i)).append('\"');
-                    }
-                    queryBuf.append(')');
-                }
-
-            }
-        }
-        if (StringUtil.isNotBlank(form.sort)) {
-            queryBuf.append(" sort:").append(form.sort);
-        }
-        if (form.lang != null) {
-            final Set<String> langSet = new HashSet<>();
-            for (final String lang : form.lang) {
-                if (StringUtil.isNotBlank(lang) && lang.length() < 1000) {
-                    if (Constants.ALL_LANGUAGES.equalsIgnoreCase(lang)) {
-                        langSet.add(Constants.ALL_LANGUAGES);
-                    } else {
-                        final String normalizeLang = systemHelper.normalizeLang(lang);
-                        if (normalizeLang != null) {
-                            langSet.add(normalizeLang);
-                        }
-                    }
-                }
-            }
-            if (langSet.size() > 1 && langSet.contains(Constants.ALL_LANGUAGES)) {
-                langSet.clear();
-                form.lang = new String[] { Constants.ALL_LANGUAGES };
-            } else {
-                langSet.remove(Constants.ALL_LANGUAGES);
-            }
-            appendLangQuery(queryBuf, langSet);
-        } else if (Constants.TRUE.equals(crawlerProperties.getProperty(Constants.USE_BROWSER_LOCALE_FOR_SEARCH_PROPERTY, Constants.FALSE))) {
-            final Set<String> langSet = new HashSet<>();
-            final Enumeration<Locale> locales = request.getLocales();
-            if (locales != null) {
-                while (locales.hasMoreElements()) {
-                    final Locale locale = locales.nextElement();
-                    final String normalizeLang = systemHelper.normalizeLang(locale.toString());
-                    if (normalizeLang != null) {
-                        langSet.add(normalizeLang);
-                    }
+            } catch (final InvalidQueryException e) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug(e.getMessage(), e);
                 }
-                if (!langSet.isEmpty()) {
-                    appendLangQuery(queryBuf, langSet);
+                throwValidationError(e.getMessageCode(), () -> asHtml(path_ErrorJsp));
+            } catch (final ResultOffsetExceededException e) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug(e.getMessage(), e);
                 }
+                throwValidationError(messages -> {
+                    messages.addErrorsResultSizeExceeded(GLOBAL);
+                }, () -> asHtml(path_ErrorJsp));
             }
-        }
-
-        final String query = queryBuf.toString().trim();
+            form.rt = Long.toString(systemHelper.getCurrentTimeAsLong());
+            data.register("displayQuery", getDisplayQuery(form, labelTypeHelper.getLabelTypeItemList()));
+            data.register("pagingQuery", getPagingQuery(form));
+        });
+    }
 
-        // init pager
-        if (StringUtil.isBlank(form.start)) {
-            form.start = String.valueOf(DEFAULT_START_COUNT);
-        } else {
+    protected HtmlResponse doMove(final SearchForm form, final int move) {
+        int start = queryHelper.getDefaultStart();
+        if (StringUtil.isNotBlank(form.pn)) {
             try {
-                Integer.parseInt(form.start);
-            } catch (final NumberFormatException e) {
-                form.start = String.valueOf(DEFAULT_START_COUNT);
-            }
-        }
-        if (StringUtil.isBlank(form.num)) {
-            form.num = String.valueOf(getDefaultPageSize());
-        }
-        normalizePageNum(form);
-
-        final int pageStart = Integer.parseInt(form.start);
-        final int pageNum = Integer.parseInt(form.num);
-        List<Map<String, Object>> documentItems = null;
-        try {
-            documentItems =
-                    fessEsClient.search(fieldHelper.docIndex, fieldHelper.docType,
-                            searchRequestBuilder -> {
-                                return SearchConditionBuilder.builder(searchRequestBuilder).query(query).offset(pageStart).size(pageNum)
-                                        .facetInfo(form.facet).geoInfo(form.geo).responseFields(queryHelper.getResponseFields()).build();
-                            }, (searchRequestBuilder, execTime, searchResponse) -> {
-                                final QueryResponseList queryResponseList = ComponentUtil.getQueryResponseList();
-                                queryResponseList.init(searchResponse, pageStart, pageNum);
-                                return queryResponseList;
-                            });
-        } catch (final InvalidQueryException e) {
-            if (logger.isDebugEnabled()) {
-                logger.debug(e.getMessage(), e);
-            }
-            throwValidationError(e.getMessageCode(), () -> asHtml(path_ErrorJsp));
-        } catch (final ResultOffsetExceededException e) {
-            if (logger.isDebugEnabled()) {
-                logger.debug(e.getMessage(), e);
-            }
-            throwValidationError(messages -> {
-                messages.addErrorsResultSizeExceeded(GLOBAL);
-            }, () -> asHtml(path_ErrorJsp));
-        }
-        data.register("documentItems", documentItems);
-
-        // search
-        final QueryResponseList queryResponseList = (QueryResponseList) documentItems;
-        data.register("facetResponse", queryResponseList.getFacetResponse());
-        final NumberFormat nf = NumberFormat.getInstance(LaRequestUtil.getRequest().getLocale());
-        nf.setMaximumIntegerDigits(2);
-        nf.setMaximumFractionDigits(2);
-        String execTime;
-        try {
-            execTime = nf.format((double) queryResponseList.getExecTime() / 1000);
-        } catch (final Exception e) {
-            execTime = StringUtil.EMPTY;
-        }
-        data.register("execTime", execTime);
-
-        final Clock clock = Clock.systemDefaultZone();
-        form.rt = Long.toString(clock.millis());
-
-        // favorite
-        if (favoriteSupport || screenShotManager != null) {
-            form.queryId = userInfoHelper.generateQueryId(query, documentItems);
-            if (screenShotManager != null) {
-                screenShotManager.storeRequest(form.queryId, documentItems);
-                data.register("screenShotSupport", true);
-            }
-        }
-
-        // search log
-        if (searchLogSupport) {
-            final long now = systemHelper.getCurrentTimeAsLong();
-
-            final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
-            final SearchLog searchLog = new SearchLog();
-
-            String userCode = null;
-            if (Constants.TRUE.equals(crawlerProperties.getProperty(Constants.USER_INFO_PROPERTY, Constants.TRUE))) {
-                userCode = userInfoHelper.getUserCode();
-                if (StringUtil.isNotBlank(userCode)) {
-                    final UserInfo userInfo = new UserInfo();
-                    userInfo.setCode(userCode);
-                    userInfo.setCreatedTime(now);
-                    userInfo.setUpdatedTime(now);
-                    searchLog.setUserInfo(OptionalEntity.of(userInfo));
-                }
-            }
-
-            searchLog.setHitCount(queryResponseList.getAllRecordCount());
-            searchLog.setResponseTime(Integer.valueOf((int) queryResponseList.getExecTime()));
-            searchLog.setSearchWord(StringUtils.abbreviate(query, 1000));
-            searchLog.setSearchQuery(StringUtils.abbreviate(queryResponseList.getSearchQuery(), 1000));
-            searchLog.setRequestedTime(now);
-            searchLog.setQueryOffset(pageStart);
-            searchLog.setQueryPageSize(pageNum);
-
-            searchLog.setClientIp(StringUtils.abbreviate(request.getRemoteAddr(), 50));
-            searchLog.setReferer(StringUtils.abbreviate(request.getHeader("referer"), 1000));
-            searchLog.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 255));
-            if (userCode != null) {
-                searchLog.setUserSessionId(userCode);
-            }
-            final Object accessType = request.getAttribute(Constants.SEARCH_LOG_ACCESS_TYPE);
-            if (Constants.SEARCH_LOG_ACCESS_TYPE_JSON.equals(accessType)) {
-                searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_JSON);
-            } else if (Constants.SEARCH_LOG_ACCESS_TYPE_XML.equals(accessType)) {
-                searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_XML);
-            } else if (Constants.SEARCH_LOG_ACCESS_TYPE_OTHER.equals(accessType)) {
-                searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_OTHER);
-            } else {
-                searchLog.setAccessType(Constants.SEARCH_LOG_ACCESS_TYPE_WEB);
-            }
-
-            @SuppressWarnings("unchecked")
-            final Map<String, List<String>> fieldLogMap = (Map<String, List<String>>) request.getAttribute(Constants.FIELD_LOGS);
-            if (fieldLogMap != null) {
-                for (final Map.Entry<String, List<String>> logEntry : fieldLogMap.entrySet()) {
-                    for (final String value : logEntry.getValue()) {
-                        searchLog.addSearchFieldLogValue(logEntry.getKey(), StringUtils.abbreviate(value, 1000));
+                int pageNumber = Integer.parseInt(form.pn);
+                if (pageNumber > 0) {
+                    pageNumber = pageNumber + move;
+                    if (pageNumber < 1) {
+                        pageNumber = 1;
                     }
+                    start = (pageNumber - 1) * form.getPageSize();
                 }
+            } catch (final NumberFormatException e) {
+                // ignore
             }
-
-            searchLogHelper.addSearchLog(searchLog);
-        }
-
-        final String[] highlightQueries = (String[]) request.getAttribute(Constants.HIGHLIGHT_QUERIES);
-        if (highlightQueries != null) {
-            final StringBuilder buf = new StringBuilder(100);
-            for (final String q : highlightQueries) {
-                buf.append("&hq=").append(q);
-            }
-            data.register("appendHighlightQueries", buf.toString());
         }
+        form.start = String.valueOf(start);
 
-        data.register("pageSize", queryResponseList.getPageSize());
-        data.register("currentPageNumber", queryResponseList.getCurrentPageNumber());
-        data.register("allRecordCount", queryResponseList.getAllRecordCount());
-        data.register("allPageCount", queryResponseList.getAllPageCount());
-        data.register("existNextPage", queryResponseList.isExistNextPage());
-        data.register("existPrevPage", queryResponseList.isExistPrevPage());
-        data.register("currentStartRecordNumber", queryResponseList.getCurrentStartRecordNumber());
-        data.register("currentEndRecordNumber", queryResponseList.getCurrentEndRecordNumber());
-        data.register("pageNumberList", queryResponseList.getPageNumberList());
-        data.register("partialResults", queryResponseList.isPartialResults());
-        // TODO
-        //        data.register("queryTime", queryResponseList.get);
-        //        data.register("searchTime", queryResponseList.get);
-
-        return query;
-    }
-
-    protected void appendLangQuery(final StringBuilder queryBuf, final Set<String> langSet) {
-        if (langSet.size() == 1) {
-            queryBuf.append(' ').append(fieldHelper.langField).append(':').append(langSet.iterator().next());
-        } else if (langSet.size() > 1) {
-            boolean first = true;
-            for (final String lang : langSet) {
-                if (first) {
-                    queryBuf.append(" (");
-                    first = false;
-                } else {
-                    queryBuf.append(" OR ");
-                }
-                queryBuf.append(fieldHelper.langField).append(':').append(lang);
-            }
-            queryBuf.append(')');
-        }
+        return doSearch(form);
     }
 
     protected void updateSearchParams(final SearchForm form) {
@@ -472,57 +218,12 @@ public class SearchAction extends FessSearchAction {
         return buf.toString();
     }
 
-    protected void normalizePageNum(final SearchForm form) {
-        try {
-            final int num = Integer.parseInt(form.num);
-            if (num > getMaxPageSize()) {
-                // max page size
-                form.num = String.valueOf(getMaxPageSize());
-            } else if (num <= 0) {
-                form.num = String.valueOf(getDefaultPageSize());
-            }
-        } catch (final NumberFormatException e) {
-            form.num = String.valueOf(getDefaultPageSize());
-        }
-    }
-
-    protected int getDefaultPageSize() {
-        return DEFAULT_PAGE_SIZE;
-    }
-
-    protected int getMaxPageSize() {
-        final Object maxPageSize = crawlerProperties.get(Constants.SEARCH_RESULT_MAX_PAGE_SIZE);
-        if (maxPageSize == null) {
-            return MAX_PAGE_SIZE;
-        }
-        try {
-            return Integer.parseInt(maxPageSize.toString());
-        } catch (final NumberFormatException e) {
-            return MAX_PAGE_SIZE;
-        }
-    }
-
-    protected boolean hasFieldInQuery(final Set<String> fieldSet, final String query) {
-        final Matcher matcher = FIELD_EXTRACTION_PATTERN.matcher(query);
-        if (matcher.matches()) {
-            final String field = matcher.replaceFirst("$1");
-            if (fieldSet.contains(field)) {
-                return true;
-            }
-            fieldSet.add(field);
-        }
-        return false;
-    }
-
     protected String getPagingQuery(final SearchForm form) {
         final StringBuilder buf = new StringBuilder(200);
         if (form.additional != null) {
-            final Set<String> fieldSet = new HashSet<String>();
-            for (final String additional : form.additional) {
-                if (StringUtil.isNotBlank(additional) && additional.length() < 1000 && !hasFieldInQuery(fieldSet, additional)) {
-                    buf.append("&additional=").append(LaFunctions.u(additional));
-                }
-            }
+            searchService.appendAdditionalQuery(form.additional, additional -> {
+                buf.append("&additional=").append(LaFunctions.u(additional));
+            });
         }
         if (StringUtil.isNotBlank(form.sort)) {
             buf.append("&sort=").append(LaFunctions.u(form.sort));
@@ -566,4 +267,107 @@ public class SearchAction extends FessSearchAction {
         return buf.toString();
     }
 
+    protected static class WebRenderData extends SearchRenderData {
+        private final RenderData data;
+
+        WebRenderData(final RenderData data) {
+            this.data = data;
+        }
+
+        @Override
+        public void setDocumentItems(final List<Map<String, Object>> documentItems) {
+            data.register("documentItems", documentItems);
+            super.setDocumentItems(documentItems);
+        }
+
+        @Override
+        public void setFacetResponse(final FacetResponse facetResponse) {
+            data.register("facetResponse", facetResponse);
+            super.setFacetResponse(facetResponse);
+        }
+
+        @Override
+        public void setAppendHighlightParams(final String appendHighlightParams) {
+            data.register("appendHighlightParams", appendHighlightParams);
+            super.setAppendHighlightParams(appendHighlightParams);
+        }
+
+        @Override
+        public void setExecTime(final String execTime) {
+            data.register("execTime", execTime);
+            super.setExecTime(execTime);
+        }
+
+        @Override
+        public void setPageSize(final int pageSize) {
+            data.register("pageSize", pageSize);
+            super.setPageSize(pageSize);
+        }
+
+        @Override
+        public void setCurrentPageNumber(final int currentPageNumber) {
+            data.register("currentPageNumber", currentPageNumber);
+            super.setCurrentPageNumber(currentPageNumber);
+        }
+
+        @Override
+        public void setAllRecordCount(final long allRecordCount) {
+            data.register("allRecordCount", allRecordCount);
+            super.setAllRecordCount(allRecordCount);
+        }
+
+        @Override
+        public void setAllPageCount(final int allPageCount) {
+            data.register("allPageCount", allPageCount);
+            super.setAllPageCount(allPageCount);
+        }
+
+        @Override
+        public void setExistNextPage(final boolean existNextPage) {
+            data.register("existNextPage", existNextPage);
+            super.setExistNextPage(existNextPage);
+        }
+
+        @Override
+        public void setExistPrevPage(final boolean existPrevPage) {
+            data.register("existPrevPage", existPrevPage);
+            super.setExistPrevPage(existPrevPage);
+        }
+
+        @Override
+        public void setCurrentStartRecordNumber(final long currentStartRecordNumber) {
+            data.register("currentStartRecordNumber", currentStartRecordNumber);
+            super.setCurrentStartRecordNumber(currentStartRecordNumber);
+        }
+
+        @Override
+        public void setCurrentEndRecordNumber(final long currentEndRecordNumber) {
+            data.register("currentEndRecordNumber", currentEndRecordNumber);
+            super.setCurrentEndRecordNumber(currentEndRecordNumber);
+        }
+
+        @Override
+        public void setPageNumberList(final List<String> pageNumberList) {
+            data.register("pageNumberList", pageNumberList);
+            super.setPageNumberList(pageNumberList);
+        }
+
+        @Override
+        public void setPartialResults(final boolean partialResults) {
+            data.register("partialResults", partialResults);
+            super.setPartialResults(partialResults);
+        }
+
+        @Override
+        public void setQueryTime(final long queryTime) {
+            data.register("queryTime", queryTime);
+            super.setQueryTime(queryTime);
+        }
+
+        @Override
+        public void setSearchQuery(final String searchQuery) {
+            data.register("searchQuery", searchQuery);
+            super.setSearchQuery(searchQuery);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/codelibs/fess/app/web/search/SearchForm.java b/src/main/java/org/codelibs/fess/app/web/search/SearchForm.java
index 13825d254df16b1d78de0cc841d9bc2ab3831285..c058a9624937a272b90a4ff45ff721dfb17aaf76 100644
--- a/src/main/java/org/codelibs/fess/app/web/search/SearchForm.java
+++ b/src/main/java/org/codelibs/fess/app/web/search/SearchForm.java
@@ -20,10 +20,14 @@ import java.io.Serializable;
 import java.util.HashMap;
 import java.util.Map;
 
+import org.codelibs.core.lang.StringUtil;
 import org.codelibs.fess.entity.FacetInfo;
 import org.codelibs.fess.entity.GeoInfo;
+import org.codelibs.fess.entity.SearchRequestParams;
+import org.codelibs.fess.helper.QueryHelper;
+import org.codelibs.fess.util.ComponentUtil;
 
-public class SearchForm implements Serializable {
+public class SearchForm implements SearchRequestParams, Serializable {
 
     private static final long serialVersionUID = 1L;
 
@@ -94,4 +98,90 @@ public class SearchForm implements Serializable {
 
     public Map<String, String[]> options = new HashMap<>();
 
+    private int startPosition = -1;
+
+    private int pageSize = -1;
+
+    @Override
+    public int getStartPosition() {
+        if (startPosition != -1) {
+            return startPosition;
+        }
+
+        final QueryHelper queryHelper = ComponentUtil.getQueryHelper();
+        if (StringUtil.isBlank(start)) {
+            startPosition = queryHelper.getDefaultStart();
+        } else {
+            try {
+                startPosition = Integer.parseInt(start);
+            } catch (final NumberFormatException e) {
+                startPosition = queryHelper.getDefaultStart();
+            }
+        }
+        start = String.valueOf(startPosition);
+        return startPosition;
+    }
+
+    @Override
+    public int getPageSize() {
+        if (pageSize != -1) {
+            return pageSize;
+        }
+
+        final QueryHelper queryHelper = ComponentUtil.getQueryHelper();
+        if (StringUtil.isBlank(num)) {
+            pageSize = queryHelper.getDefaultPageSize();
+        } else {
+            try {
+                pageSize = Integer.parseInt(num);
+                if (pageSize > queryHelper.getMaxPageSize() || pageSize <= 0) {
+                    pageSize = queryHelper.getMaxPageSize();
+                }
+            } catch (final NumberFormatException e) {
+                pageSize = queryHelper.getDefaultPageSize();
+            }
+        }
+        num = String.valueOf(pageSize);
+        return pageSize;
+    }
+
+    @Override
+    public String getQuery() {
+        return query;
+    }
+
+    @Override
+    public String getOperator() {
+        return op;
+    }
+
+    @Override
+    public String[] getAdditional() {
+        return additional;
+    }
+
+    @Override
+    public Map<String, String[]> getFields() {
+        return fields;
+    }
+
+    @Override
+    public String[] getLanguages() {
+        return lang;
+    }
+
+    @Override
+    public GeoInfo getGeoInfo() {
+        return geo;
+    }
+
+    @Override
+    public FacetInfo getFacetInfo() {
+        return facet;
+    }
+
+    @Override
+    public String getSort() {
+        return sort;
+    }
 }
diff --git a/src/main/java/org/codelibs/fess/dict/DictionaryCreator.java b/src/main/java/org/codelibs/fess/dict/DictionaryCreator.java
index b1b9d2d429edc554b213626eac29581ee711bde4..2761958fe2f8ce77260db47ac1d8417f80a9f4d3 100644
--- a/src/main/java/org/codelibs/fess/dict/DictionaryCreator.java
+++ b/src/main/java/org/codelibs/fess/dict/DictionaryCreator.java
@@ -11,11 +11,11 @@ public abstract class DictionaryCreator {
 
     protected DictionaryManager dictionaryManager;
 
-    public DictionaryCreator(String pattern) {
+    public DictionaryCreator(final String pattern) {
         this.pattern = Pattern.compile(pattern);
     }
 
-    public DictionaryFile<? extends DictionaryItem> create(String path, Date timestamp) {
+    public DictionaryFile<? extends DictionaryItem> create(final String path, final Date timestamp) {
         if (!isTarget(path)) {
             return null;
         }
@@ -23,17 +23,17 @@ public abstract class DictionaryCreator {
         return newDictionaryFile(encodePath(path), path, timestamp);
     }
 
-    protected String encodePath(String path) {
+    protected String encodePath(final String path) {
         return Base64.getEncoder().encodeToString(path.getBytes(Constants.CHARSET_UTF_8));
     }
 
-    protected boolean isTarget(String path) {
+    protected boolean isTarget(final String path) {
         return pattern.matcher(path).find();
     }
 
     protected abstract DictionaryFile<? extends DictionaryItem> newDictionaryFile(String id, String path, Date timestamp);
 
-    public void setDictionaryManager(DictionaryManager dictionaryManager) {
+    public void setDictionaryManager(final DictionaryManager dictionaryManager) {
         this.dictionaryManager = dictionaryManager;
     }
 }
diff --git a/src/main/java/org/codelibs/fess/dict/DictionaryFile.java b/src/main/java/org/codelibs/fess/dict/DictionaryFile.java
index 5ae99f946dcc0394f97d266307e536958932db85..13f98948351a553aa99920c0a3aad29d919ed953 100644
--- a/src/main/java/org/codelibs/fess/dict/DictionaryFile.java
+++ b/src/main/java/org/codelibs/fess/dict/DictionaryFile.java
@@ -34,7 +34,7 @@ public abstract class DictionaryFile<T extends DictionaryItem> {
 
     protected Date timestamp;
 
-    public DictionaryFile(String id, String path, Date timestamp) {
+    public DictionaryFile(final String id, final String path, final Date timestamp) {
         this.id = id;
         this.path = path;
         this.timestamp = timestamp;
@@ -52,7 +52,7 @@ public abstract class DictionaryFile<T extends DictionaryItem> {
         return timestamp;
     }
 
-    public DictionaryFile<T> manager(DictionaryManager dictionaryManager) {
+    public DictionaryFile<T> manager(final DictionaryManager dictionaryManager) {
         this.dictionaryManager = dictionaryManager;
         return this;
     }
diff --git a/src/main/java/org/codelibs/fess/dict/DictionaryManager.java b/src/main/java/org/codelibs/fess/dict/DictionaryManager.java
index 644e3907e52188da16bef6a02686b6b508ca5f6f..1940dcc6a219f5bfe599334c5b8ed2c02a0d96b3 100644
--- a/src/main/java/org/codelibs/fess/dict/DictionaryManager.java
+++ b/src/main/java/org/codelibs/fess/dict/DictionaryManager.java
@@ -54,35 +54,35 @@ public class DictionaryManager {
 
     public DictionaryFile<? extends DictionaryItem>[] getDictionaryFiles() {
         try (CurlResponse response = Curl.get(getUrl() + "/_configsync/file").param("fields", "path,@timestamp").execute()) {
-            Map<String, Object> contentMap = response.getContentAsMap();
+            final Map<String, Object> contentMap = response.getContentAsMap();
             @SuppressWarnings("unchecked")
-            List<Map<String, Object>> fileList = (List<Map<String, Object>>) contentMap.get("file");
+            final List<Map<String, Object>> fileList = (List<Map<String, Object>>) contentMap.get("file");
             return fileList
                     .stream()
                     .map(fileMap -> {
                         try {
-                            String path = fileMap.get("path").toString();
-                            Date timestamp =
+                            final String path = fileMap.get("path").toString();
+                            final Date timestamp =
                                     new SimpleDateFormat(Constants.DATE_FORMAT_ISO_8601_EXTEND_UTC).parse(fileMap.get("@timestamp")
                                             .toString());
                             for (final DictionaryCreator creator : creatorList) {
-                                DictionaryFile<? extends DictionaryItem> file = creator.create(path, timestamp);
+                                final DictionaryFile<? extends DictionaryItem> file = creator.create(path, timestamp);
                                 if (file != null) {
                                     return file;
                                 }
                             }
-                        } catch (Exception e) {
+                        } catch (final Exception e) {
                             logger.warn("Failed to load " + fileMap, e);
                         }
                         return null;
                     }).filter(file -> file != null).toArray(n -> new DictionaryFile<?>[n]);
-        } catch (IOException e) {
+        } catch (final IOException e) {
             throw new DictionaryException("Failed to access dictionaries", e);
         }
     }
 
     public OptionalEntity<DictionaryFile<? extends DictionaryItem>> getDictionaryFile(final String id) {
-        for (DictionaryFile<? extends DictionaryItem> dictFile : getDictionaryFiles()) {
+        for (final DictionaryFile<? extends DictionaryItem> dictFile : getDictionaryFiles()) {
             if (dictFile.getId().equals(id)) {
                 return OptionalEntity.of(dictFile);
             }
@@ -90,7 +90,7 @@ public class DictionaryManager {
         return OptionalEntity.empty();
     }
 
-    public void store(DictionaryFile<? extends DictionaryItem> dictFile, File file) {
+    public void store(final DictionaryFile<? extends DictionaryItem> dictFile, final File file) {
         getDictionaryFile(dictFile.getId())
                 .ifPresent(currentFile -> {
                     if (currentFile.getTimestamp().getTime() > dictFile.getTimestamp().getTime()) {
@@ -101,11 +101,11 @@ public class DictionaryManager {
                         try (CurlResponse response =
                                 Curl.post(getUrl() + "/_configsync/file").param("path", dictFile.getPath()).body(FileUtil.readUTF8(file))
                                         .execute()) {
-                            Map<String, Object> contentMap = response.getContentAsMap();
+                            final Map<String, Object> contentMap = response.getContentAsMap();
                             if (!Constants.TRUE.equalsIgnoreCase(contentMap.get("acknowledged").toString())) {
                                 throw new DictionaryException("Failed to update " + dictFile.getPath());
                             }
-                        } catch (IOException e) {
+                        } catch (final IOException e) {
                             throw new DictionaryException("Failed to update " + dictFile.getPath(), e);
                         }
 
@@ -114,15 +114,15 @@ public class DictionaryManager {
                 });
     }
 
-    public InputStream getContentInputStream(DictionaryFile<? extends DictionaryItem> dictFile) {
+    public InputStream getContentInputStream(final DictionaryFile<? extends DictionaryItem> dictFile) {
         try {
             return Curl.get(getUrl() + "/_configsync/file").param("path", dictFile.getPath()).execute().getContentAsStream();
-        } catch (IOException e) {
+        } catch (final IOException e) {
             throw new DictionaryException("Failed to access " + dictFile.getPath(), e);
         }
     }
 
-    public void addCreator(DictionaryCreator creator) {
+    public void addCreator(final DictionaryCreator creator) {
         creatorList.add(creator);
     }
 
diff --git a/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiCreator.java b/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiCreator.java
index f10d162b2d41f644ace31f2b228af8449fb3b0d7..a784ccf6b8cedf9eb42cc6634b37dcbe942f41d4 100644
--- a/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiCreator.java
+++ b/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiCreator.java
@@ -12,12 +12,12 @@ public class KuromojiCreator extends DictionaryCreator {
         super("kuromoji.*\\.txt");
     }
 
-    public KuromojiCreator(String pattern) {
+    public KuromojiCreator(final String pattern) {
         super(pattern);
     }
 
     @Override
-    protected DictionaryFile<? extends DictionaryItem> newDictionaryFile(String id, String path, Date timestamp) {
+    protected DictionaryFile<? extends DictionaryItem> newDictionaryFile(final String id, final String path, final Date timestamp) {
         return new KuromojiFile(id, path, timestamp).manager(dictionaryManager);
     }
 
diff --git a/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiFile.java b/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiFile.java
index 81ea1717f20d23676cc22ad12da3657d292ae8eb..62373f371d49cc6bb7a7539a41b37aad73703031 100644
--- a/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiFile.java
+++ b/src/main/java/org/codelibs/fess/dict/kuromoji/KuromojiFile.java
@@ -45,7 +45,7 @@ public class KuromojiFile extends DictionaryFile<KuromojiItem> {
 
     List<KuromojiItem> kuromojiItemList;
 
-    public KuromojiFile(String id, String path, Date timestamp) {
+    public KuromojiFile(final String id, final String path, final Date timestamp) {
         super(id, path, timestamp);
     }
 
@@ -114,7 +114,7 @@ public class KuromojiFile extends DictionaryFile<KuromojiItem> {
         }
     }
 
-    protected void reload(final KuromojiUpdater updater, InputStream in) {
+    protected void reload(final KuromojiUpdater updater, final InputStream in) {
         final List<KuromojiItem> itemList = new ArrayList<KuromojiItem>();
         try (BufferedReader reader =
                 new BufferedReader(new InputStreamReader(in != null ? in : dictionaryManager.getContentInputStream(this), Constants.UTF_8))) {
@@ -164,7 +164,7 @@ public class KuromojiFile extends DictionaryFile<KuromojiItem> {
                 }
             }
             if (updater != null) {
-                KuromojiItem item = updater.commit();
+                final KuromojiItem item = updater.commit();
                 if (item != null) {
                     itemList.add(item);
                 }
diff --git a/src/main/java/org/codelibs/fess/dict/synonym/SynonymCreator.java b/src/main/java/org/codelibs/fess/dict/synonym/SynonymCreator.java
index daec4cf5ed4cce6722d35d1fd22aa70baba04f5b..771f4b43eb3c7c9a2ce098dddf4521671ba2d5f4 100644
--- a/src/main/java/org/codelibs/fess/dict/synonym/SynonymCreator.java
+++ b/src/main/java/org/codelibs/fess/dict/synonym/SynonymCreator.java
@@ -12,12 +12,12 @@ public class SynonymCreator extends DictionaryCreator {
         super("synonym.*\\.txt");
     }
 
-    public SynonymCreator(String pattern) {
+    public SynonymCreator(final String pattern) {
         super(pattern);
     }
 
     @Override
-    protected DictionaryFile<? extends DictionaryItem> newDictionaryFile(String id, String path, Date timestamp) {
+    protected DictionaryFile<? extends DictionaryItem> newDictionaryFile(final String id, final String path, final Date timestamp) {
         return new SynonymFile(id, path, timestamp).manager(dictionaryManager);
     }
 
diff --git a/src/main/java/org/codelibs/fess/dict/synonym/SynonymFile.java b/src/main/java/org/codelibs/fess/dict/synonym/SynonymFile.java
index a76c667d4550727c1e32f0fa3d89d37c6a651d42..58895c07fd77e81573998682bdbf6ae61bacad3a 100644
--- a/src/main/java/org/codelibs/fess/dict/synonym/SynonymFile.java
+++ b/src/main/java/org/codelibs/fess/dict/synonym/SynonymFile.java
@@ -44,7 +44,7 @@ public class SynonymFile extends DictionaryFile<SynonymItem> {
 
     List<SynonymItem> synonymItemList;
 
-    public SynonymFile(String id, String path, Date timestamp) {
+    public SynonymFile(final String id, final String path, final Date timestamp) {
         super(id, path, timestamp);
     }
 
@@ -114,7 +114,7 @@ public class SynonymFile extends DictionaryFile<SynonymItem> {
         }
     }
 
-    protected void reload(final SynonymUpdater updater, InputStream in) {
+    protected void reload(final SynonymUpdater updater, final InputStream in) {
         final List<SynonymItem> itemList = new ArrayList<SynonymItem>();
         try (BufferedReader reader =
                 new BufferedReader(new InputStreamReader(in != null ? in : dictionaryManager.getContentInputStream(this), Constants.UTF_8))) {
@@ -186,7 +186,7 @@ public class SynonymFile extends DictionaryFile<SynonymItem> {
                 }
             }
             if (updater != null) {
-                SynonymItem item = updater.commit();
+                final SynonymItem item = updater.commit();
                 if (item != null) {
                     itemList.add(item);
                 }
diff --git a/src/main/java/org/codelibs/fess/entity/SearchRenderData.java b/src/main/java/org/codelibs/fess/entity/SearchRenderData.java
new file mode 100644
index 0000000000000000000000000000000000000000..addb3b8c4dd78a675f2054c0d6d13a977222dfb1
--- /dev/null
+++ b/src/main/java/org/codelibs/fess/entity/SearchRenderData.java
@@ -0,0 +1,170 @@
+package org.codelibs.fess.entity;
+
+import java.util.List;
+import java.util.Map;
+
+import org.codelibs.fess.util.FacetResponse;
+
+public class SearchRenderData {
+
+    private List<Map<String, Object>> documentItems;
+
+    private FacetResponse facetResponse;
+
+    private String appendHighlightParams;
+
+    private String execTime;
+
+    private int pageSize;
+
+    private int currentPageNumber;
+
+    private long allRecordCount;
+
+    private int allPageCount;
+
+    private boolean existNextPage;
+
+    private boolean existPrevPage;
+
+    private long currentStartRecordNumber;
+
+    private long currentEndRecordNumber;
+
+    private List<String> pageNumberList;
+
+    private boolean partialResults;
+
+    private String searchQuery;
+
+    private long queryTime;
+
+    public void setDocumentItems(final List<Map<String, Object>> documentItems) {
+        this.documentItems = documentItems;
+    }
+
+    public void setFacetResponse(final FacetResponse facetResponse) {
+        this.facetResponse = facetResponse;
+    }
+
+    public void setAppendHighlightParams(final String appendHighlightParams) {
+        this.appendHighlightParams = appendHighlightParams;
+    }
+
+    public void setExecTime(final String execTime) {
+        this.execTime = execTime;
+    }
+
+    public void setPageSize(final int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public void setCurrentPageNumber(final int currentPageNumber) {
+        this.currentPageNumber = currentPageNumber;
+    }
+
+    public void setAllRecordCount(final long allRecordCount) {
+        this.allRecordCount = allRecordCount;
+    }
+
+    public void setAllPageCount(final int allPageCount) {
+        this.allPageCount = allPageCount;
+    }
+
+    public void setExistNextPage(final boolean existNextPage) {
+        this.existNextPage = existNextPage;
+    }
+
+    public void setExistPrevPage(final boolean existPrevPage) {
+        this.existPrevPage = existPrevPage;
+    }
+
+    public void setCurrentStartRecordNumber(final long currentStartRecordNumber) {
+        this.currentStartRecordNumber = currentStartRecordNumber;
+    }
+
+    public void setCurrentEndRecordNumber(final long currentEndRecordNumber) {
+        this.currentEndRecordNumber = currentEndRecordNumber;
+    }
+
+    public void setPageNumberList(final List<String> pageNumberList) {
+        this.pageNumberList = pageNumberList;
+    }
+
+    public void setPartialResults(final boolean partialResults) {
+        this.partialResults = partialResults;
+    }
+
+    public void setQueryTime(final long queryTime) {
+        this.queryTime = queryTime;
+    }
+
+    public void setSearchQuery(final String searchQuery) {
+        this.searchQuery = searchQuery;
+    }
+
+    public List<Map<String, Object>> getDocumentItems() {
+        return documentItems;
+    }
+
+    public FacetResponse getFacetResponse() {
+        return facetResponse;
+    }
+
+    public String getAppendHighlightParams() {
+        return appendHighlightParams;
+    }
+
+    public String getExecTime() {
+        return execTime;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public int getCurrentPageNumber() {
+        return currentPageNumber;
+    }
+
+    public long getAllRecordCount() {
+        return allRecordCount;
+    }
+
+    public int getAllPageCount() {
+        return allPageCount;
+    }
+
+    public boolean isExistNextPage() {
+        return existNextPage;
+    }
+
+    public boolean isExistPrevPage() {
+        return existPrevPage;
+    }
+
+    public long getCurrentStartRecordNumber() {
+        return currentStartRecordNumber;
+    }
+
+    public long getCurrentEndRecordNumber() {
+        return currentEndRecordNumber;
+    }
+
+    public List<String> getPageNumberList() {
+        return pageNumberList;
+    }
+
+    public boolean isPartialResults() {
+        return partialResults;
+    }
+
+    public String getSearchQuery() {
+        return searchQuery;
+    }
+
+    public long getQueryTime() {
+        return queryTime;
+    }
+
+}
diff --git a/src/main/java/org/codelibs/fess/entity/SearchRequestParams.java b/src/main/java/org/codelibs/fess/entity/SearchRequestParams.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f4616dd5716def344b0c8a3cfdca8c12efc7acb
--- /dev/null
+++ b/src/main/java/org/codelibs/fess/entity/SearchRequestParams.java
@@ -0,0 +1,27 @@
+package org.codelibs.fess.entity;
+
+import java.util.Map;
+
+public interface SearchRequestParams {
+
+    String getQuery();
+
+    String getOperator();
+
+    String[] getAdditional();
+
+    Map<String, String[]> getFields();
+
+    String[] getLanguages();
+
+    GeoInfo getGeoInfo();
+
+    FacetInfo getFacetInfo();
+
+    String getSort();
+
+    int getStartPosition();
+
+    int getPageSize();
+
+}
diff --git a/src/main/java/org/codelibs/fess/es/client/FessEsClient.java b/src/main/java/org/codelibs/fess/es/client/FessEsClient.java
index d7f8a9004283d5b574c1c43556d1fc052231b2f7..73feeec2f75c691ae0bdb2c1a63f347c9e5d480c 100644
--- a/src/main/java/org/codelibs/fess/es/client/FessEsClient.java
+++ b/src/main/java/org/codelibs/fess/es/client/FessEsClient.java
@@ -10,7 +10,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -36,6 +35,7 @@ import org.codelibs.fess.exception.ResultOffsetExceededException;
 import org.codelibs.fess.helper.QueryHelper;
 import org.codelibs.fess.indexer.FessSearchQueryException;
 import org.codelibs.fess.util.ComponentUtil;
+import org.dbflute.optional.OptionalEntity;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.Action;
 import org.elasticsearch.action.ActionFuture;
@@ -471,7 +471,7 @@ public class FessEsClient implements Client {
         }
         final long execTime = System.currentTimeMillis() - startTime;
 
-        return searchResult.build(requestBuilder, execTime, Optional.ofNullable(response));
+        return searchResult.build(requestBuilder, execTime, OptionalEntity.ofNullable(response, () -> {/* TODO */}));
     }
 
     public <T> T search(final String index, final String type, final SearchCondition<SearchRequestBuilder> condition,
@@ -504,10 +504,10 @@ public class FessEsClient implements Client {
         }
         final long execTime = System.currentTimeMillis() - startTime;
 
-        return searchResult.build(searchRequestBuilder, execTime, Optional.ofNullable(searchResponse));
+        return searchResult.build(searchRequestBuilder, execTime, OptionalEntity.ofNullable(searchResponse, () -> {/* TODO */}));
     }
 
-    public Optional<Map<String, Object>> getDocument(final String index, final String type,
+    public OptionalEntity<Map<String, Object>> getDocument(final String index, final String type,
             final SearchCondition<SearchRequestBuilder> condition) {
         return getDocument(index, type, condition, (response, hit) -> {
             final Map<String, Object> source = hit.getSource();
@@ -522,7 +522,7 @@ public class FessEsClient implements Client {
         });
     }
 
-    public <T> Optional<T> getDocument(final String index, final String type, final SearchCondition<SearchRequestBuilder> condition,
+    public <T> OptionalEntity<T> getDocument(final String index, final String type, final SearchCondition<SearchRequestBuilder> condition,
             final EntityCreator<T, SearchResponse, SearchHit> creator) {
         return search(index, type, condition, (queryBuilder, execTime, searchResponse) -> {
             return searchResponse.map(response -> {
@@ -535,7 +535,7 @@ public class FessEsClient implements Client {
         });
     }
 
-    public Optional<Map<String, Object>> getDocument(final String index, final String type, final String id,
+    public OptionalEntity<Map<String, Object>> getDocument(final String index, final String type, final String id,
             final SearchCondition<GetRequestBuilder> condition) {
         return getDocument(index, type, id, condition, (response, result) -> {
             final Map<String, Object> source = response.getSource();
@@ -550,7 +550,7 @@ public class FessEsClient implements Client {
         });
     }
 
-    public <T> Optional<T> getDocument(final String index, final String type, final String id,
+    public <T> OptionalEntity<T> getDocument(final String index, final String type, final String id,
             final SearchCondition<GetRequestBuilder> condition, final EntityCreator<T, GetResponse, GetResponse> creator) {
         return get(index, type, id, condition, (queryBuilder, execTime, getResponse) -> {
             return getResponse.map(response -> {
@@ -562,7 +562,15 @@ public class FessEsClient implements Client {
     public List<Map<String, Object>> getDocumentList(final String index, final String type,
             final SearchCondition<SearchRequestBuilder> condition) {
         return getDocumentList(index, type, condition, (response, hit) -> {
-            return hit.getSource();
+            final Map<String, Object> source = hit.getSource();
+            if (source != null) {
+                return source;
+            }
+            final Map<String, SearchHitField> fields = hit.getFields();
+            if (fields != null) {
+                return fields.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> (Object) e.getValue().getValues()));
+            }
+            return null;
         });
     }
 
@@ -875,7 +883,7 @@ public class FessEsClient implements Client {
     }
 
     public interface SearchResult<T, B, R> {
-        T build(B requestBuilder, long execTime, Optional<R> response);
+        T build(B requestBuilder, long execTime, OptionalEntity<R> response);
     }
 
     public interface EntityCreator<T, R, H> {
diff --git a/src/main/java/org/codelibs/fess/helper/HotSearchWordHelper.java b/src/main/java/org/codelibs/fess/helper/HotSearchWordHelper.java
index 3eba43da0d8ba789130666daab3ab1944cf060f2..647a32a32d6d77adc81eedf7a58bb4b7b9b40b1a 100644
--- a/src/main/java/org/codelibs/fess/helper/HotSearchWordHelper.java
+++ b/src/main/java/org/codelibs/fess/helper/HotSearchWordHelper.java
@@ -77,5 +77,23 @@ public class HotSearchWordHelper {
         public long getTime() {
             return time;
         }
+
+        public static Range parseRange(final String value) {
+            Range range;
+            if (value == null) {
+                range = Range.ENTIRE;
+            } else if ("day".equals(value) || "1".equals(value)) {
+                range = Range.ONE_DAY;
+            } else if ("week".equals(value) || "7".equals(value)) {
+                range = Range.ONE_DAY;
+            } else if ("month".equals(value) || "30".equals(value)) {
+                range = Range.ONE_DAY;
+            } else if ("year".equals(value) || "365".equals(value)) {
+                range = Range.ONE_DAY;
+            } else {
+                range = Range.ENTIRE;
+            }
+            return range;
+        }
     }
 }
diff --git a/src/main/java/org/codelibs/fess/helper/QueryHelper.java b/src/main/java/org/codelibs/fess/helper/QueryHelper.java
index c846591b39b847705c74c9e3a566ac3bb9396c2c..15717521ee83915f72bcae895a6564417bedd1fa 100644
--- a/src/main/java/org/codelibs/fess/helper/QueryHelper.java
+++ b/src/main/java/org/codelibs/fess/helper/QueryHelper.java
@@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletRequest;
 
 import org.apache.commons.lang3.StringUtils;
 import org.codelibs.core.lang.StringUtil;
+import org.codelibs.core.misc.DynamicProperties;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.entity.FacetInfo;
 import org.codelibs.fess.entity.GeoInfo;
@@ -45,27 +46,36 @@ import org.lastaflute.web.util.LaRequestUtil;
 
 public class QueryHelper implements Serializable {
 
-    private static final String SCORE_FIELD = "score";
+    protected static final long serialVersionUID = 1L;
 
-    private static final String INURL_FIELD = "inurl";
+    protected static final String SCORE_FIELD = "score";
 
-    private static final String NOT_ = "NOT ";
+    protected static final String INURL_FIELD = "inurl";
 
-    private static final String AND = "AND";
+    protected static final String NOT_ = "NOT ";
 
-    private static final String OR = "OR";
+    protected static final String AND = "AND";
 
-    private static final String NOT = "NOT";
+    protected static final String OR = "OR";
 
-    private static final String _TO_ = " TO ";
+    protected static final String NOT = "NOT";
 
-    private static final String _OR_ = " OR ";
+    protected static final String _TO_ = " TO ";
 
-    private static final String _AND_ = " AND ";
+    protected static final String _OR_ = " OR ";
 
-    private static final String DEFAULT_OPERATOR = _AND_;
+    protected static final String _AND_ = " AND ";
 
-    private static final long serialVersionUID = 1L;
+    protected static final String DEFAULT_OPERATOR = _AND_;
+
+    protected static final int DEFAULT_START_POSITION = 0;
+
+    protected static final int DEFAULT_PAGE_SIZE = 20;
+
+    protected static final int MAX_PAGE_SIZE = 100;
+
+    @Resource
+    protected DynamicProperties crawlerProperties;
 
     @Resource
     protected RoleQueryHelper roleQueryHelper;
@@ -113,9 +123,9 @@ public class QueryHelper implements Serializable {
 
     protected long timeAllowed = -1;
 
-    protected Map<String, String[]> requestParameterMap = new HashMap<String, String[]>();
+    protected Map<String, String[]> requestParameterMap = new HashMap<>();
 
-    protected Map<String, String> fieldLanguageMap = new HashMap<String, String>();
+    protected Map<String, String> fieldLanguageMap = new HashMap<>();
 
     protected int maxSearchResultOffset = 100000;
 
@@ -133,9 +143,13 @@ public class QueryHelper implements Serializable {
 
     protected String defaultQueryLanguage;
 
-    protected Map<String, String[]> additionalQueryParamMap = new HashMap<String, String[]>();
+    protected Map<String, String[]> additionalQueryParamMap = new HashMap<>();
+
+    protected Map<String, String> fieldBoostMap = new HashMap<>();
 
-    protected Map<String, String> fieldBoostMap = new HashMap<String, String>();
+    protected int defaultPageSize = DEFAULT_PAGE_SIZE;
+
+    protected int defaultStartPosition = DEFAULT_START_POSITION;
 
     @PostConstruct
     public void init() {
@@ -1378,6 +1392,34 @@ public class QueryHelper implements Serializable {
         this.defaultQueryLanguage = defaultQueryLanguage;
     }
 
+    public int getMaxPageSize() {
+        final Object maxPageSize = crawlerProperties.get(Constants.SEARCH_RESULT_MAX_PAGE_SIZE);
+        if (maxPageSize == null) {
+            return MAX_PAGE_SIZE;
+        }
+        try {
+            return Integer.parseInt(maxPageSize.toString());
+        } catch (final NumberFormatException e) {
+            return MAX_PAGE_SIZE;
+        }
+    }
+
+    public int getDefaultPageSize() {
+        return defaultPageSize;
+    }
+
+    public void setDefaultPageSize(final int defaultPageSize) {
+        this.defaultPageSize = defaultPageSize;
+    }
+
+    public int getDefaultStart() {
+        return defaultStartPosition;
+    }
+
+    public void setDefaultStart(final int defaultStartPosition) {
+        this.defaultStartPosition = defaultStartPosition;
+    }
+
     public Map<String, String[]> getQueryParamMap() {
         if (additionalQueryParamMap.isEmpty()) {
             return additionalQueryParamMap;
diff --git a/src/main/java/org/codelibs/fess/mylasta/action/FessHtmlPath.java b/src/main/java/org/codelibs/fess/mylasta/action/FessHtmlPath.java
index 3bcb073ac86e64f22299dd818a465d1869ab8f25..9d51704e3d2fc76ae3f831ee8613ef7a291c9332 100644
--- a/src/main/java/org/codelibs/fess/mylasta/action/FessHtmlPath.java
+++ b/src/main/java/org/codelibs/fess/mylasta/action/FessHtmlPath.java
@@ -347,9 +347,6 @@ public interface FessHtmlPath {
     /** The path of the HTML: /error/badRequest.jsp */
     HtmlNext path_Error_BadRequestJsp = new HtmlNext("/error/badRequest.jsp");
 
-    /** The path of the HTML: /error/error_message.jsp */
-    HtmlNext path_Error_ErrorMessageJsp = new HtmlNext("/error/error_message.jsp");
-
     /** The path of the HTML: /error/footer.jsp */
     HtmlNext path_Error_FooterJsp = new HtmlNext("/error/footer.jsp");
 
diff --git a/src/main/java/org/codelibs/fess/mylasta/action/FessUserBean.java b/src/main/java/org/codelibs/fess/mylasta/action/FessUserBean.java
index 3613691b5405842aad92ccb8e0d68cc1c495019f..df8072f68acc1de389542077defbfd822525fc25 100644
--- a/src/main/java/org/codelibs/fess/mylasta/action/FessUserBean.java
+++ b/src/main/java/org/codelibs/fess/mylasta/action/FessUserBean.java
@@ -30,7 +30,7 @@ public class FessUserBean extends TypicalUserBean<String> { // #change_it also L
     //                                                                          ==========
     /** The serial version UID for object serialization. (Default) */
     private static final long serialVersionUID = 1L;
-    private User user;
+    private final User user;
 
     // ===================================================================================
     //                                                                           Attribute
@@ -62,19 +62,19 @@ public class FessUserBean extends TypicalUserBean<String> { // #change_it also L
         return user.getGroups();
     }
 
-    public boolean hasRole(String role) {
+    public boolean hasRole(final String role) {
         return Stream.of(user.getRoleNames()).anyMatch(s -> s.equals(role));
     }
 
-    public boolean hasRoles(String[] acceptedRoles) {
+    public boolean hasRoles(final String[] acceptedRoles) {
         return Stream.of(user.getRoleNames()).anyMatch(s1 -> Stream.of(acceptedRoles).anyMatch(s2 -> s2.equals(s1)));
     }
 
-    public boolean hasGroup(String group) {
+    public boolean hasGroup(final String group) {
         return Stream.of(user.getGroupNames()).anyMatch(s -> s.equals(group));
     }
 
-    public boolean hasGroups(String[] acceptedGroups) {
+    public boolean hasGroups(final String[] acceptedGroups) {
         return Stream.of(user.getGroupNames()).anyMatch(s1 -> Stream.of(acceptedGroups).anyMatch(s2 -> s2.equals(s1)));
     }
 }
diff --git a/src/main/java/org/codelibs/fess/util/ComponentUtil.java b/src/main/java/org/codelibs/fess/util/ComponentUtil.java
index 3e3404d17e831ac95c8f32b3791a3cd5a374b0cc..fe2a8b481db97df68f25c8e7309ed8a4059d6826 100644
--- a/src/main/java/org/codelibs/fess/util/ComponentUtil.java
+++ b/src/main/java/org/codelibs/fess/util/ComponentUtil.java
@@ -45,6 +45,7 @@ import org.codelibs.fess.helper.SambaHelper;
 import org.codelibs.fess.helper.SearchLogHelper;
 import org.codelibs.fess.helper.SystemHelper;
 import org.codelibs.fess.helper.UserAgentHelper;
+import org.codelibs.fess.helper.UserInfoHelper;
 import org.codelibs.fess.helper.ViewHelper;
 import org.codelibs.fess.indexer.IndexUpdater;
 import org.codelibs.fess.job.JobExecutor;
@@ -54,6 +55,8 @@ import org.lastaflute.di.core.factory.SingletonLaContainerFactory;
 import org.lastaflute.web.servlet.session.SessionManager;
 
 public final class ComponentUtil {
+    private static final String FESS_ES_CLIENT = "fessEsClient";
+
     private static final String DICTIONARY_MANAGER = "dictionaryManager";
 
     private static final String DATA_SERVICE = "dataService";
@@ -76,6 +79,8 @@ public final class ComponentUtil {
 
     private static final String USER_AGENT_HELPER = "userAgentHelper";
 
+    private static final String USER_INFO_HELPER = "userInfoHelper";
+
     private static final String WEB_API_MANAGER_FACTORY = "webApiManagerFactory";
 
     private static final String JOB_HELPER = "jobHelper";
@@ -118,7 +123,7 @@ public final class ComponentUtil {
 
     private static final String FIELD_HELPER = "fieldHelper";
 
-    private static final String ELASTICSEARCH_CLIENT = "fessEsClient";
+    private static final String ELASTICSEARCH_CLIENT = FESS_ES_CLIENT;
 
     private ComponentUtil() {
     }
@@ -243,6 +248,10 @@ public final class ComponentUtil {
         return SingletonLaContainer.getComponent(FIELD_HELPER);
     }
 
+    public static UserInfoHelper getUserInfoHelper() {
+        return SingletonLaContainer.getComponent(USER_INFO_HELPER);
+    }
+
     public static FessEsClient getElasticsearchClient() {
         return SingletonLaContainer.getComponent(ELASTICSEARCH_CLIENT);
     }
@@ -253,13 +262,16 @@ public final class ComponentUtil {
 
     public static DictionaryManager getDictionaryManager() {
         return SingletonLaContainer.getComponent(DICTIONARY_MANAGER);
-
     }
 
     public static DataService<EsAccessResult> getDataService() {
         return SingletonLaContainer.getComponent(DATA_SERVICE);
     }
 
+    public static FessEsClient getFessEsClient() {
+        return SingletonLaContainer.getComponent(FESS_ES_CLIENT);
+    }
+
     public static FessLoginAssist getLoginAssist() {
         return getComponent(FessLoginAssist.class);
     }
diff --git a/src/main/java/org/codelibs/fess/util/DocumentUtil.java b/src/main/java/org/codelibs/fess/util/DocumentUtil.java
index 0dd94840bab88c3062c2a0251842c79404331fb0..c3689327b7b935d1837fc1b96f32a656d27824e9 100644
--- a/src/main/java/org/codelibs/fess/util/DocumentUtil.java
+++ b/src/main/java/org/codelibs/fess/util/DocumentUtil.java
@@ -38,7 +38,7 @@ public final class DocumentUtil {
     }
 
     @SuppressWarnings("unchecked")
-    private static <T> T convertObj(Object value, final Class<T> clazz) {
+    private static <T> T convertObj(final Object value, final Class<T> clazz) {
         if (value == null) {
             return null;
         }
diff --git a/src/main/java/org/codelibs/fess/util/QueryResponseList.java b/src/main/java/org/codelibs/fess/util/QueryResponseList.java
index dd30a6227bdf18c446ba4841b8d2901f6c4944c7..24f04988dcdc53274198d16ec0de31c50c1f49d6 100644
--- a/src/main/java/org/codelibs/fess/util/QueryResponseList.java
+++ b/src/main/java/org/codelibs/fess/util/QueryResponseList.java
@@ -23,11 +23,11 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
-import java.util.Optional;
 
 import org.apache.commons.lang3.StringUtils;
 import org.codelibs.fess.helper.QueryHelper;
 import org.codelibs.fess.helper.ViewHelper;
+import org.dbflute.optional.OptionalEntity;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.common.text.Text;
 import org.elasticsearch.search.SearchHit;
@@ -82,7 +82,7 @@ public class QueryResponseList implements List<Map<String, Object>> {
         this.parent = parent;
     }
 
-    public void init(final Optional<SearchResponse> searchResponseOpt, final int start, final int pageSize) {
+    public void init(final OptionalEntity<SearchResponse> searchResponseOpt, final int start, final int pageSize) {
         searchResponseOpt.ifPresent(searchResponse -> {
             final SearchHits searchHits = searchResponse.getHits();
             allRecordCount = searchHits.getTotalHits();
@@ -362,4 +362,8 @@ public class QueryResponseList implements List<Map<String, Object>> {
         return partialResults;
     }
 
+    public long getQueryTime() {
+        return queryTime;
+    }
+
 }
diff --git a/src/main/java/org/codelibs/fess/util/ResourceUtil.java b/src/main/java/org/codelibs/fess/util/ResourceUtil.java
index 95965591427524f3e94c1f128679c22efc0cd87f..3f247752e96244dab0dab1e9f176e6adf09e443c 100644
--- a/src/main/java/org/codelibs/fess/util/ResourceUtil.java
+++ b/src/main/java/org/codelibs/fess/util/ResourceUtil.java
@@ -59,13 +59,13 @@ public class ResourceUtil {
         return getPath("dict", names);
     }
 
-    protected static Path getPath(final String base, String... names) {
+    protected static Path getPath(final String base, final String... names) {
 
         try {
             final ServletContext servletContext = SingletonLaContainer.getComponent(ServletContext.class);
-            String webinfoPath = servletContext.getRealPath("/WEB-INF/" + base);
+            final String webinfoPath = servletContext.getRealPath("/WEB-INF/" + base);
             if (webinfoPath != null) {
-                Path path = Paths.get(webinfoPath, names);
+                final Path path = Paths.get(webinfoPath, names);
                 if (Files.exists(path)) {
                     return path;
                 }
diff --git a/src/main/resources/fess_api.xml b/src/main/resources/fess_api.xml
index 6f46798dd3ef4e67fef146cdf341bce843ee8d48..a2bf55dfec521434481b57c5b6843b3305d4bdf7 100644
--- a/src/main/resources/fess_api.xml
+++ b/src/main/resources/fess_api.xml
@@ -5,9 +5,6 @@
 	<include path="fess_config.xml"/>
 
 	<component name="webApiManagerFactory" class="org.codelibs.fess.api.WebApiManagerFactory">
-		<postConstruct name="add">
-			<arg>xmlApiManager</arg>
-		</postConstruct>
 		<postConstruct name="add">
 			<arg>jsonApiManager</arg>
 		</postConstruct>
@@ -19,8 +16,6 @@
 		</postConstruct>
 	</component>
 
-	<component name="xmlApiManager" class="org.codelibs.fess.api.xml.XmlApiManager">
-	</component>
 	<component name="jsonApiManager" class="org.codelibs.fess.api.json.JsonApiManager">
 	</component>
 	<component name="esApiManager" class="org.codelibs.fess.api.es.EsApiManager">
diff --git a/src/main/webapp/WEB-INF/view/searchResults.jsp b/src/main/webapp/WEB-INF/view/searchResults.jsp
index ec251ad1ee606fcc8f5e3dc9f1e582d12674fce7..a87d161b49b58d8c3945df47116a4d8ae47dc09c 100644
--- a/src/main/webapp/WEB-INF/view/searchResults.jsp
+++ b/src/main/webapp/WEB-INF/view/searchResults.jsp
@@ -69,7 +69,7 @@
 								<a href="#${doc.doc_id}" class="favorite"><la:message
 										key="labels.search_result_favorite" /> (${f:h(doc.favorite_count)})</a>
 								<span class="favorited"><la:message
-										key="labels.search_result_favorited"/> <span class="favorited-count">(${f:h(doc.favoriteCount_l_x_dv)})</span></span>
+										key="labels.search_result_favorited"/> <span class="favorited-count">(${f:h(doc.favorite_count)})</span></span>
 							</c:if>
 						</div>
 					</div>