Browse Source

fix #1873 add aggregations for search logs

Shinsuke Sugaya 6 years ago
parent
commit
31f64e09e8

+ 20 - 0
src/main/java/org/codelibs/fess/app/pager/SearchLogPager.java

@@ -26,8 +26,28 @@ public class SearchLogPager implements Serializable {
 
     public static final String LOG_TYPE_SEARCH = "search";
 
+    public static final String LOG_TYPE_SEARCH_COUNT_HOUR = "search_count_hour_agg";
+
+    public static final String LOG_TYPE_SEARCH_COUNT_DAY = "search_count_day_agg";
+
+    public static final String LOG_TYPE_SEARCH_USER_HOUR = "search_user_hour_agg";
+
+    public static final String LOG_TYPE_SEARCH_USER_DAY = "search_user_day_agg";
+
+    public static final String LOG_TYPE_SEARCH_REQTIMEAVG_HOUR = "search_reqtimeavg_hour_agg";
+
+    public static final String LOG_TYPE_SEARCH_REQTIMEAVG_DAY = "search_reqtimeavg_day_agg";
+
+    public static final String LOG_TYPE_SEARCH_KEYWORD = "search_keyword_agg";
+
+    public static final String LOG_TYPE_SEARCH_ZEROHIT = "search_zerohit_agg";
+
+    public static final String LOG_TYPE_SEARCH_ZEROCLICK = "search_zeroclick_agg";
+
     public static final String LOG_TYPE_CLICK = "click";
 
+    public static final String LOG_TYPE_CLICK_TOP = "click_top_agg";
+
     public static final String LOG_TYPE_FAVORITE = "favorite";
 
     public static final int DEFAULT_PAGE_SIZE = 20;

+ 304 - 77
src/main/java/org/codelibs/fess/app/service/SearchLogService.java

@@ -15,12 +15,17 @@
  */
 package org.codelibs.fess.app.service;
 
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
+import java.util.Base64;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import javax.annotation.Resource;
 
@@ -28,6 +33,10 @@ import org.codelibs.core.beans.util.BeanUtil;
 import org.codelibs.core.lang.StringUtil;
 import org.codelibs.fess.Constants;
 import org.codelibs.fess.app.pager.SearchLogPager;
+import org.codelibs.fess.es.log.allcommon.EsPagingResultBean;
+import org.codelibs.fess.es.log.cbean.ClickLogCB;
+import org.codelibs.fess.es.log.cbean.FavoriteLogCB;
+import org.codelibs.fess.es.log.cbean.SearchLogCB;
 import org.codelibs.fess.es.log.exbhv.ClickLogBhv;
 import org.codelibs.fess.es.log.exbhv.FavoriteLogBhv;
 import org.codelibs.fess.es.log.exbhv.SearchLogBhv;
@@ -38,12 +47,29 @@ import org.codelibs.fess.exception.FessSystemException;
 import org.codelibs.fess.helper.SystemHelper;
 import org.codelibs.fess.mylasta.direction.FessConfig;
 import org.codelibs.fess.taglib.FessFunctions;
-import org.dbflute.cbean.result.PagingResultBean;
 import org.dbflute.optional.OptionalEntity;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.BucketOrder;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.aggregations.metrics.avg.Avg;
+import org.elasticsearch.search.aggregations.metrics.cardinality.Cardinality;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class SearchLogService {
+
+    private static final String COUNT = "count";
+
+    private static final String KEY = "key";
+
+    private static final String ID = "id";
+
+    private static final String USER_INFO_ID = "userInfoId";
+
+    private static final String QUERY_TIME = "queryTime";
+
     private static final Logger logger = LoggerFactory.getLogger(SearchLogService.class);
 
     @Resource
@@ -67,91 +93,205 @@ public class SearchLogService {
         });
     }
 
-    public PagingResultBean<?> getSearchLogList(final SearchLogPager pager) {
-        final PagingResultBean<?> list;
-        if (SearchLogPager.LOG_TYPE_CLICK.equalsIgnoreCase(pager.logType)) {
-            list = clickLogBhv.selectPage(cb -> {
+    public List<?> getSearchLogList(final SearchLogPager pager) {
+        final EsPagingResultBean<?> list;
+        if (SearchLogPager.LOG_TYPE_CLICK.equalsIgnoreCase(pager.logType)
+                || SearchLogPager.LOG_TYPE_CLICK_TOP.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) clickLogBhv.selectPage(cb -> {
                 cb.paging(pager.getPageSize(), pager.getCurrentPageNumber());
                 cb.query().addOrderBy_RequestedAt_Desc();
-                if (StringUtil.isNotBlank(pager.queryId)) {
-                    cb.query().setQueryId_Term(pager.queryId);
-                }
-                if (StringUtil.isNotBlank(pager.userSessionId)) {
-                    cb.query().setUserSessionId_Term(pager.userSessionId);
-                }
-                if (StringUtil.isNotBlank(pager.requestedTimeRange)) {
-                    String[] values = pager.requestedTimeRange.split(" - ");
-                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
-                    try {
-                        if (values.length > 0) {
-                            cb.query().setRequestedAt_GreaterEqual(LocalDateTime.parse(values[0], formatter));
-                        }
-                        if (values.length > 1) {
-                            cb.query().setRequestedAt_LessEqual(LocalDateTime.parse(values[1], formatter));
-                        }
-                    } catch (Exception e) {
-                        if (logger.isDebugEnabled()) {
-                            logger.debug("Failed to parse " + pager.requestedTimeRange, e);
-                        }
-                    }
-                }
+                createClickLogCondition(pager, cb);
+            });
+        } else if (SearchLogPager.LOG_TYPE_CLICK_TOP.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) clickLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createClickLogCondition(pager, cb);
+                cb.aggregation().filter("top_click_filter", null, null, aggs -> {
+                    aggs.setUrl_Count();
+                });
             });
         } else if (SearchLogPager.LOG_TYPE_FAVORITE.equalsIgnoreCase(pager.logType)) {
-            list = favoriteLogBhv.selectPage(cb -> {
+            list = (EsPagingResultBean<?>) favoriteLogBhv.selectPage(cb -> {
                 cb.paging(pager.getPageSize(), pager.getCurrentPageNumber());
                 cb.query().addOrderBy_CreatedAt_Desc();
-                if (StringUtil.isNotBlank(pager.queryId)) {
-                    cb.query().setQueryId_Term(pager.queryId);
-                }
-                if (StringUtil.isNotBlank(pager.userSessionId)) {
-                    cb.query().setUserInfoId_Term(pager.userSessionId);
-                }
-                if (StringUtil.isNotBlank(pager.requestedTimeRange)) {
-                    String[] values = pager.requestedTimeRange.split(" - ");
-                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
-                    try {
-                        if (values.length > 0) {
-                            cb.query().setCreatedAt_GreaterEqual(LocalDateTime.parse(values[0], formatter));
-                        }
-                        if (values.length > 1) {
-                            cb.query().setCreatedAt_LessEqual(parseDateTime(values[1], formatter));
-                        }
-                    } catch (Exception e) {
-                        if (logger.isDebugEnabled()) {
-                            logger.debug("Failed to parse " + pager.requestedTimeRange, e);
-                        }
-                    }
-                }
+                createFavoriteLogCondition(pager, cb);
             });
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_COUNT_HOUR.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setRequestedAt_DateHistogram(SearchLogPager.LOG_TYPE_SEARCH_COUNT_HOUR, op -> {
+                    op.dateHistogramInterval(DateHistogramInterval.HOUR);
+                    op.minDocCount(0);
+                    op.order(BucketOrder.key(true));
+                }, null);
+            });
+            final Histogram agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_COUNT_HOUR);
+            final List<? extends Histogram.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                map.put(COUNT, e.getDocCount());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_COUNT_DAY.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setRequestedAt_DateHistogram(SearchLogPager.LOG_TYPE_SEARCH_COUNT_DAY, op -> {
+                    op.dateHistogramInterval(DateHistogramInterval.DAY);
+                    op.minDocCount(0);
+                    op.order(BucketOrder.key(true));
+                }, null);
+            });
+            final Histogram agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_COUNT_DAY);
+            final List<? extends Histogram.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                map.put(COUNT, e.getDocCount());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_USER_HOUR.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setRequestedAt_DateHistogram(SearchLogPager.LOG_TYPE_SEARCH_USER_HOUR, op -> {
+                    op.dateHistogramInterval(DateHistogramInterval.HOUR);
+                    op.subAggregation(AggregationBuilders.cardinality(USER_INFO_ID).field(USER_INFO_ID));
+                    op.minDocCount(0);
+                    op.order(BucketOrder.key(true));
+                }, null);
+            });
+            final Histogram agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_USER_HOUR);
+            final List<? extends Histogram.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                final Cardinality value = e.getAggregations().get(USER_INFO_ID);
+                map.put(COUNT, value.getValue());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_USER_DAY.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setRequestedAt_DateHistogram(SearchLogPager.LOG_TYPE_SEARCH_USER_DAY, op -> {
+                    op.dateHistogramInterval(DateHistogramInterval.DAY);
+                    op.subAggregation(AggregationBuilders.cardinality(USER_INFO_ID).field(USER_INFO_ID));
+                    op.minDocCount(0);
+                    op.order(BucketOrder.key(true));
+                }, null);
+            });
+            final Histogram agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_USER_DAY);
+            final List<? extends Histogram.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                final Cardinality value = e.getAggregations().get(USER_INFO_ID);
+                map.put(COUNT, value.getValue());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_REQTIMEAVG_HOUR.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setRequestedAt_DateHistogram(SearchLogPager.LOG_TYPE_SEARCH_REQTIMEAVG_HOUR, op -> {
+                    op.dateHistogramInterval(DateHistogramInterval.HOUR);
+                    op.subAggregation(AggregationBuilders.avg(QUERY_TIME).field(QUERY_TIME));
+                    op.minDocCount(0);
+                    op.order(BucketOrder.key(true));
+                }, null);
+            });
+            final Histogram agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_REQTIMEAVG_HOUR);
+            final List<? extends Histogram.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                final Avg value = e.getAggregations().get(QUERY_TIME);
+                map.put(COUNT, value.getValueAsString());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_REQTIMEAVG_DAY.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setRequestedAt_DateHistogram(SearchLogPager.LOG_TYPE_SEARCH_REQTIMEAVG_DAY, op -> {
+                    op.dateHistogramInterval(DateHistogramInterval.DAY);
+                    op.subAggregation(AggregationBuilders.avg(QUERY_TIME).field(QUERY_TIME));
+                    op.minDocCount(0);
+                    op.order(BucketOrder.key(true));
+                }, null);
+            });
+            final Histogram agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_REQTIMEAVG_DAY);
+            final List<? extends Histogram.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                final Avg value = e.getAggregations().get(QUERY_TIME);
+                map.put(COUNT, value.getValueAsString());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_KEYWORD.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.aggregation().setSearchWord_Terms(SearchLogPager.LOG_TYPE_SEARCH_KEYWORD, op -> {
+                    op.size(pager.getPageSize());
+                }, null);
+            });
+            final Terms agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_KEYWORD);
+            final List<? extends Terms.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                map.put(COUNT, e.getDocCount());
+                return map;
+            }).collect(Collectors.toList());
+        } else if (SearchLogPager.LOG_TYPE_SEARCH_ZEROHIT.equalsIgnoreCase(pager.logType)) {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+                cb.fetchFirst(0);
+                createSearchLogCondition(pager, cb);
+                cb.query().setHitCount_Equal(0L);
+                cb.aggregation().setSearchWord_Terms(SearchLogPager.LOG_TYPE_SEARCH_ZEROHIT, op -> {
+                    op.size(pager.getPageSize());
+                }, null);
+            });
+            final Terms agg = list.getAggregations().get(SearchLogPager.LOG_TYPE_SEARCH_ZEROHIT);
+            final List<? extends Terms.Bucket> buckets = agg.getBuckets();
+            updatePagerByAgg(pager, buckets.size());
+            return buckets.stream().map(e -> {
+                final Map<String, Object> map = new HashMap<>();
+                map.put(ID, Base64.getUrlEncoder().encodeToString(e.getKeyAsString().getBytes(StandardCharsets.UTF_8)));
+                map.put(KEY, e.getKeyAsString());
+                map.put(COUNT, e.getDocCount());
+                return map;
+            }).collect(Collectors.toList());
+            //        } else if (SearchLogPager.LOG_TYPE_SEARCH_ZEROCLICK.equalsIgnoreCase(pager.logType)) {
+            //            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
+            //                cb.fetchFirst(0);
+            //                createSearchLogCondition(pager, cb);
+            //                // TODO 0 clicked
+            //                });
         } else {
-            list = searchLogBhv.selectPage(cb -> {
+            list = (EsPagingResultBean<?>) searchLogBhv.selectPage(cb -> {
                 cb.paging(pager.getPageSize(), pager.getCurrentPageNumber());
                 cb.query().addOrderBy_RequestedAt_Desc();
-                if (StringUtil.isNotBlank(pager.queryId)) {
-                    cb.query().setQueryId_Term(pager.queryId);
-                }
-                if (StringUtil.isNotBlank(pager.userSessionId)) {
-                    cb.query().setUserSessionId_Term(pager.userSessionId);
-                }
-                if (StringUtil.isNotBlank(pager.accessType)) {
-                    cb.query().setAccessType_Term(pager.accessType);
-                }
-                if (StringUtil.isNotBlank(pager.requestedTimeRange)) {
-                    String[] values = pager.requestedTimeRange.split(" - ");
-                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
-                    try {
-                        if (values.length > 0) {
-                            cb.query().setRequestedAt_GreaterEqual(parseDateTime(values[0], formatter));
-                        }
-                        if (values.length > 1) {
-                            cb.query().setRequestedAt_LessEqual(LocalDateTime.parse(values[1], formatter));
-                        }
-                    } catch (Exception e) {
-                        if (logger.isDebugEnabled()) {
-                            logger.debug("Failed to parse " + pager.requestedTimeRange, e);
-                        }
-                    }
-                }
+                createSearchLogCondition(pager, cb);
             });
         }
 
@@ -164,7 +304,94 @@ public class SearchLogService {
         return list;
     }
 
-    protected LocalDateTime parseDateTime(String value, DateTimeFormatter formatter) {
+    private void updatePagerByAgg(final SearchLogPager pager, final int size) {
+        pager.setAllPageCount(1);
+        pager.setAllRecordCount(size);
+        pager.setCurrentPageNumber(1);
+        pager.setExistNextPage(false);
+        pager.setExistPrePage(false);
+        pager.setPageSize(pager.getPageSize());
+    }
+
+    private void createSearchLogCondition(final SearchLogPager pager, final SearchLogCB cb) {
+        if (StringUtil.isNotBlank(pager.queryId)) {
+            cb.query().setQueryId_Term(pager.queryId);
+        }
+        if (StringUtil.isNotBlank(pager.userSessionId)) {
+            cb.query().setUserSessionId_Term(pager.userSessionId);
+        }
+        if (StringUtil.isNotBlank(pager.accessType)) {
+            cb.query().setAccessType_Term(pager.accessType);
+        }
+        if (StringUtil.isNotBlank(pager.requestedTimeRange)) {
+            final String[] values = pager.requestedTimeRange.split(" - ");
+            final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+            try {
+                if (values.length > 0) {
+                    cb.query().setRequestedAt_GreaterEqual(parseDateTime(values[0], formatter));
+                }
+                if (values.length > 1) {
+                    cb.query().setRequestedAt_LessEqual(LocalDateTime.parse(values[1], formatter));
+                }
+            } catch (final Exception e) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Failed to parse " + pager.requestedTimeRange, e);
+                }
+            }
+        }
+    }
+
+    private void createFavoriteLogCondition(final SearchLogPager pager, final FavoriteLogCB cb) {
+        if (StringUtil.isNotBlank(pager.queryId)) {
+            cb.query().setQueryId_Term(pager.queryId);
+        }
+        if (StringUtil.isNotBlank(pager.userSessionId)) {
+            cb.query().setUserInfoId_Term(pager.userSessionId);
+        }
+        if (StringUtil.isNotBlank(pager.requestedTimeRange)) {
+            final String[] values = pager.requestedTimeRange.split(" - ");
+            final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+            try {
+                if (values.length > 0) {
+                    cb.query().setCreatedAt_GreaterEqual(LocalDateTime.parse(values[0], formatter));
+                }
+                if (values.length > 1) {
+                    cb.query().setCreatedAt_LessEqual(parseDateTime(values[1], formatter));
+                }
+            } catch (final Exception e) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Failed to parse " + pager.requestedTimeRange, e);
+                }
+            }
+        }
+    }
+
+    private void createClickLogCondition(final SearchLogPager pager, final ClickLogCB cb) {
+        if (StringUtil.isNotBlank(pager.queryId)) {
+            cb.query().setQueryId_Term(pager.queryId);
+        }
+        if (StringUtil.isNotBlank(pager.userSessionId)) {
+            cb.query().setUserSessionId_Term(pager.userSessionId);
+        }
+        if (StringUtil.isNotBlank(pager.requestedTimeRange)) {
+            final String[] values = pager.requestedTimeRange.split(" - ");
+            final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+            try {
+                if (values.length > 0) {
+                    cb.query().setRequestedAt_GreaterEqual(LocalDateTime.parse(values[0], formatter));
+                }
+                if (values.length > 1) {
+                    cb.query().setRequestedAt_LessEqual(LocalDateTime.parse(values[1], formatter));
+                }
+            } catch (final Exception e) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Failed to parse " + pager.requestedTimeRange, e);
+                }
+            }
+        }
+    }
+
+    protected LocalDateTime parseDateTime(final String value, final DateTimeFormatter formatter) {
         return LocalDateTime.parse(value, formatter).atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
     }
 

+ 14 - 0
src/main/java/org/codelibs/fess/es/config/allcommon/EsSqlClause.java

@@ -69,4 +69,18 @@ public class EsSqlClause extends AbstractSqlClause {
     protected String createSqlSuffix() {
         return null;
     }
+
+    @Override
+    public void fetchFirst(int fetchSize) {
+        _fetchScopeEffective = true;
+        if (fetchSize < 0) {
+            String msg = "Argument[fetchSize] should be plus: " + fetchSize;
+            throw new IllegalArgumentException(msg);
+        }
+        _fetchStartIndex = 0;
+        _fetchSize = fetchSize;
+        _fetchPageNumber = 1;
+        doClearFetchPageClause();
+        doFetchFirst();
+    }
 }

+ 14 - 0
src/main/java/org/codelibs/fess/es/log/allcommon/EsSqlClause.java

@@ -69,4 +69,18 @@ public class EsSqlClause extends AbstractSqlClause {
     protected String createSqlSuffix() {
         return null;
     }
+
+    @Override
+    public void fetchFirst(int fetchSize) {
+        _fetchScopeEffective = true;
+        if (fetchSize < 0) {
+            String msg = "Argument[fetchSize] should be plus: " + fetchSize;
+            throw new IllegalArgumentException(msg);
+        }
+        _fetchStartIndex = 0;
+        _fetchSize = fetchSize;
+        _fetchPageNumber = 1;
+        doClearFetchPageClause();
+        doFetchFirst();
+    }
 }

+ 14 - 0
src/main/java/org/codelibs/fess/es/user/allcommon/EsSqlClause.java

@@ -69,4 +69,18 @@ public class EsSqlClause extends AbstractSqlClause {
     protected String createSqlSuffix() {
         return null;
     }
+
+    @Override
+    public void fetchFirst(int fetchSize) {
+        _fetchScopeEffective = true;
+        if (fetchSize < 0) {
+            String msg = "Argument[fetchSize] should be plus: " + fetchSize;
+            throw new IllegalArgumentException(msg);
+        }
+        _fetchStartIndex = 0;
+        _fetchSize = fetchSize;
+        _fetchPageNumber = 1;
+        doClearFetchPageClause();
+        doFetchFirst();
+    }
 }

+ 33 - 0
src/main/java/org/codelibs/fess/mylasta/action/FessLabels.java

@@ -2835,12 +2835,45 @@ public class FessLabels extends UserMessages {
     /** The key of the message: Favorite Log */
     public static final String LABELS_searchlog_log_type_favorite = "{labels.searchlog_log_type_favorite}";
 
+    /** The key of the message: Keywords */
+    public static final String LABELS_searchlog_log_type_search_keyword = "{labels.searchlog_log_type_search_keyword}";
+
+    /** The key of the message: Zero Hits */
+    public static final String LABELS_searchlog_log_type_search_zerohit = "{labels.searchlog_log_type_search_zerohit}";
+
+    /** The key of the message: Zero Clicks */
+    public static final String LABELS_searchlog_log_type_search_zeroclick = "{labels.searchlog_log_type_search_zeroclick}";
+
+    /** The key of the message: Keyword by Hour */
+    public static final String LABELS_searchlog_log_type_search_count_hour = "{labels.searchlog_log_type_search_count_hour}";
+
+    /** The key of the message: Keyword by Day */
+    public static final String LABELS_searchlog_log_type_search_count_day = "{labels.searchlog_log_type_search_count_day}";
+
+    /** The key of the message: User by Hour */
+    public static final String LABELS_searchlog_log_type_search_user_hour = "{labels.searchlog_log_type_search_user_hour}";
+
+    /** The key of the message: User by Day */
+    public static final String LABELS_searchlog_log_type_search_user_day = "{labels.searchlog_log_type_search_user_day}";
+
+    /** The key of the message: Request Time Avg. by Hour */
+    public static final String LABELS_searchlog_log_type_search_reqtimeavg_hour = "{labels.searchlog_log_type_search_reqtimeavg_hour}";
+
+    /** The key of the message: Request Time Avg. by Day */
+    public static final String LABELS_searchlog_log_type_search_reqtimeavg_day = "{labels.searchlog_log_type_search_reqtimeavg_day}";
+
     /** The key of the message: Message */
     public static final String LABELS_searchlog_log_message = "{labels.searchlog_log_message}";
 
     /** The key of the message: Time */
     public static final String LABELS_searchlog_requested_time = "{labels.searchlog_requested_time}";
 
+    /** The key of the message: Value */
+    public static final String LABELS_searchlog_value = "{labels.searchlog_value}";
+
+    /** The key of the message: Count */
+    public static final String LABELS_searchlog_count = "{labels.searchlog_count}";
+
     /** The key of the message: Log Details */
     public static final String LABELS_searchlog_configuration_details = "{labels.searchlog_configuration_details}";
 

+ 11 - 0
src/main/resources/fess_label.properties

@@ -936,8 +936,19 @@ labels.searchlog_log_type=Log Type
 labels.searchlog_log_type_search=Search Log
 labels.searchlog_log_type_click=Click Log
 labels.searchlog_log_type_favorite=Favorite Log
+labels.searchlog_log_type_search_keyword=Keywords
+labels.searchlog_log_type_search_zerohit=Zero Hits
+labels.searchlog_log_type_search_zeroclick=Zero Clicks
+labels.searchlog_log_type_search_count_hour=Keyword by Hour
+labels.searchlog_log_type_search_count_day=Keyword by Day
+labels.searchlog_log_type_search_user_hour=User by Hour
+labels.searchlog_log_type_search_user_day=User by Day
+labels.searchlog_log_type_search_reqtimeavg_hour=Request Time Avg. by Hour
+labels.searchlog_log_type_search_reqtimeavg_day=Request Time Avg. by Day
 labels.searchlog_log_message=Message
 labels.searchlog_requested_time=Time
+labels.searchlog_value=Value
+labels.searchlog_count=Count
 labels.searchlog_configuration_details=Log Details
 labels.searchlog_configuration_link_top=Search Log
 labels.searchlog_configuration_link_details=Details

+ 11 - 0
src/main/resources/fess_label_en.properties

@@ -936,8 +936,19 @@ labels.searchlog_log_type=Log Type
 labels.searchlog_log_type_search=Search Log
 labels.searchlog_log_type_click=Click Log
 labels.searchlog_log_type_favorite=Favorite Log
+labels.searchlog_log_type_search_keyword=Keywords
+labels.searchlog_log_type_search_zerohit=Zero Hits
+labels.searchlog_log_type_search_zeroclick=Zero Clicks
+labels.searchlog_log_type_search_count_hour=Keyword by Hour
+labels.searchlog_log_type_search_count_day=Keyword by Day
+labels.searchlog_log_type_search_user_hour=User by Hour
+labels.searchlog_log_type_search_user_day=User by Day
+labels.searchlog_log_type_search_reqtimeavg_hour=Request Time Avg. by Hour
+labels.searchlog_log_type_search_reqtimeavg_day=Request Time Avg. by Day
 labels.searchlog_log_message=Message
 labels.searchlog_requested_time=Time
+labels.searchlog_value=Value
+labels.searchlog_count=Count
 labels.searchlog_configuration_details=Log Details
 labels.searchlog_configuration_link_top=Search Log
 labels.searchlog_configuration_link_details=Details

+ 11 - 0
src/main/resources/fess_label_ja.properties

@@ -936,8 +936,19 @@ labels.searchlog_log_type=ログ種別
 labels.searchlog_log_type_search=検索ログ
 labels.searchlog_log_type_click=クリックログ
 labels.searchlog_log_type_favorite=お気に入りログ
+labels.searchlog_log_type_search_keyword=キーワード数
+labels.searchlog_log_type_search_zerohit=ゼロ件ヒット数
+labels.searchlog_log_type_search_zeroclick=ゼロ件クリック数
+labels.searchlog_log_type_search_count_hour=検索数/時
+labels.searchlog_log_type_search_count_day=検索数/日
+labels.searchlog_log_type_search_user_hour=ユーザー数/時
+labels.searchlog_log_type_search_user_day=ユーザー数/日
+labels.searchlog_log_type_search_reqtimeavg_hour=リクエスト平均時間/時
+labels.searchlog_log_type_search_reqtimeavg_day=リクエスト平均時間/日
 labels.searchlog_log_message=メッセージ
 labels.searchlog_requested_time=時刻
+labels.searchlog_value=値
+labels.searchlog_count=件数
 labels.searchlog_configuration_details=ログ詳細
 labels.searchlog_configuration_link_top=検索ログ
 labels.searchlog_configuration_link_details=詳細

+ 36 - 0
src/main/webapp/WEB-INF/view/admin/searchlog/admin_searchlog.jsp

@@ -53,6 +53,14 @@
 												<la:option value="search"><la:message key="labels.searchlog_log_type_search" /></la:option>
 												<la:option value="click"><la:message key="labels.searchlog_log_type_click" /></la:option>
 												<la:option value="favorite"><la:message key="labels.searchlog_log_type_favorite" /></la:option>
+												<la:option value="search_count_hour_agg"><la:message key="labels.searchlog_log_type_search_count_hour" /></la:option>
+												<la:option value="search_count_day_agg"><la:message key="labels.searchlog_log_type_search_count_day" /></la:option>
+												<la:option value="search_user_hour_agg"><la:message key="labels.searchlog_log_type_search_user_hour" /></la:option>
+												<la:option value="search_user_day_agg"><la:message key="labels.searchlog_log_type_search_user_day" /></la:option>
+												<la:option value="search_reqtimeavg_hour_agg"><la:message key="labels.searchlog_log_type_search_reqtimeavg_hour" /></la:option>
+												<la:option value="search_reqtimeavg_day_agg"><la:message key="labels.searchlog_log_type_search_reqtimeavg_day" /></la:option>
+												<la:option value="search_keyword_agg"><la:message key="labels.searchlog_log_type_search_keyword" /></la:option>
+												<la:option value="search_zerohit_agg"><la:message key="labels.searchlog_log_type_search_zerohit" /></la:option>
 											</la:select>
 										</div>
 									</div>
@@ -116,20 +124,48 @@
 											<table class="table table-bordered table-striped dataTable">
 												<thead>
 													<tr>
+														<c:if test="${!logType.endsWith('_agg')}">
 														<th class="col-sm-3"><la:message
 																key="labels.searchlog_requested_time" /></th>
 														<th><la:message
 																key="labels.searchlog_log_message" /></th>
+														</c:if>
+														<c:if test="${logType.startsWith('search_count_') or logType.startsWith('search_user_')}">
+														<th><la:message
+																key="labels.searchlog_requested_time" /></th>
+														<th class="col-sm-3"><la:message
+																key="labels.searchlog_count" /></th>
+														</c:if>
+														<c:if test="${logType.startsWith('search_reqtimeavg_')}">
+														<th><la:message
+																key="labels.searchlog_requested_time" /></th>
+														<th class="col-sm-3"><la:message
+																key="labels.searchlog_value" /></th>
+														</c:if>
+														<c:if test="${logType.startsWith('search_keyword_') or logType.startsWith('search_zerohit_')}">
+														<th><la:message
+																key="labels.searchlog_value" /></th>
+														<th class="col-sm-3"><la:message
+																key="labels.searchlog_count" /></th>
+														</c:if>
 													</tr>
 												</thead>
 												<tbody>
 													<c:forEach var="data" varStatus="s"
 														items="${searchLogItems}">
+														<c:if test="${!logType.endsWith('_agg')}">
 														<tr
 															data-href="${contextPath}/admin/searchlog/details/4/${f:u(logType)}/${f:u(data.id)}">
 															<td>${f:h(data.requestedAt)}</td>
 															<td>${f:h(data.logMessage)}</td>
 														</tr>
+														</c:if>
+														<c:if test="${logType.endsWith('_agg')}">
+														<tr>
+															<td>${f:h(data.key)}</td>
+															<td>${f:h(data.count)}</td>
+														</tr>
+														</c:if>
 													</c:forEach>
 												</tbody>
 											</table>