support the elasticsearch as search engine

This commit is contained in:
jbai 2016-05-20 17:59:28 -07:00
parent d06d732e1f
commit fba2c90c33
12 changed files with 2564 additions and 84 deletions

View File

@ -16,8 +16,10 @@ package controllers.api.v1;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dao.AdvSearchDAO;
import dao.SearchDAO;
import org.apache.commons.lang3.StringUtils;
import play.Logger;
import play.Play;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Result;
@ -137,17 +139,46 @@ public class AdvSearch extends Controller
}
}
result.put("status", "ok");
String searchEngine = Play.application().configuration().getString(SearchDAO.WHEREHOWS_SEARCH_ENGINE__KEY);
if (searchOpt != null && searchOpt.has("category"))
{
String category = searchOpt.get("category").asText();
if(category.equalsIgnoreCase("flow"))
{
result.set("result", Json.toJson(AdvSearchDAO.searchFlows(searchOpt, page, size)));
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", Json.toJson(AdvSearchDAO.elasticSearchFlowJobs(searchOpt, page, size)));
}
else
{
result.set("result", Json.toJson(AdvSearchDAO.searchFlows(searchOpt, page, size)));
}
return ok(result);
}
else if(category.equalsIgnoreCase("metric"))
{
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", Json.toJson(AdvSearchDAO.elasticSearchMetric(searchOpt, page, size)));
}
else
{
result.set("result", Json.toJson(AdvSearchDAO.searchMetrics(searchOpt, page, size)));
}
return ok(result);
}
}
result.set("result", Json.toJson(AdvSearchDAO.search(searchOpt, page, size)));
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", Json.toJson(AdvSearchDAO.elasticSearch(searchOpt, page, size)));
}
else
{
result.set("result", Json.toJson(AdvSearchDAO.search(searchOpt, page, size)));
}
return ok(result);
}

View File

@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import dao.SearchDAO;
import models.DatasetColumn;
import play.Play;
import play.api.libs.json.JsValue;
import play.libs.Json;
import play.mvc.Controller;
@ -92,51 +93,71 @@ public class Search extends Controller
{
category = "datasets";
}
if (StringUtils.isBlank(source))
if (StringUtils.isBlank(source) || source.equalsIgnoreCase("all") || source.equalsIgnoreCase("default"))
{
source = "all";
}
else if (source.equalsIgnoreCase("default"))
{
source = "all";
isDefault = true;
source = null;
}
String searchEngine = Play.application().configuration().getString(SearchDAO.WHEREHOWS_SEARCH_ENGINE__KEY);
if (category.toLowerCase().equalsIgnoreCase("metric"))
{
result.set("result", SearchDAO.getPagedMetricByKeyword(category, keyword, page, size));
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", SearchDAO.elasticSearchMetricByKeyword(category, keyword, page, size));
}
else
{
result.set("result", SearchDAO.getPagedMetricByKeyword(category, keyword, page, size));
}
}
else if (category.toLowerCase().equalsIgnoreCase("flows"))
{
result.set("result", SearchDAO.getPagedFlowByKeyword(category, keyword, page, size));
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", SearchDAO.elasticSearchFlowByKeyword(category, keyword, page, size));
}
else
{
result.set("result", SearchDAO.getPagedFlowByKeyword(category, keyword, page, size));
}
}
else if (category.toLowerCase().equalsIgnoreCase("jobs"))
{
result.set("result", SearchDAO.getPagedJobByKeyword(category, keyword, page, size));
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", SearchDAO.elasticSearchFlowByKeyword(category, keyword, page, size));
}
else
{
result.set("result", SearchDAO.getPagedJobByKeyword(category, keyword, page, size));
}
}
else if (category.toLowerCase().equalsIgnoreCase("comments"))
{
result.set("result", SearchDAO.getPagedCommentsByKeyword(category, keyword, page, size));
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
result.set("result", SearchDAO.elasticSearchDatasetByKeyword(category, keyword, null, page, size));
}
else
{
result.set("result", SearchDAO.getPagedCommentsByKeyword(category, keyword, page, size));
}
}
else
{
ObjectNode node = SearchDAO.getPagedDatasetByKeyword(category, keyword, source, page, size);
if (isDefault && node != null && node.has("count"))
if(StringUtils.isNotBlank(searchEngine) && searchEngine.equalsIgnoreCase("elasticsearch"))
{
Long count = node.get("count").asLong();
if (count != null && count == 0)
{
node = SearchDAO.getPagedFlowByKeyword("flows", keyword, page, size);
if (node!= null && node.has("count"))
{
Long flowCount = node.get("count").asLong();
if (flowCount != null && flowCount == 0)
{
node = SearchDAO.getPagedJobByKeyword("jobs", keyword, page, size);
}
}
}
result.set("result", SearchDAO.elasticSearchDatasetByKeyword(category, keyword, source, page, size));
}
else
{
result.set("result", SearchDAO.getPagedDatasetByKeyword(category, keyword, source, page, size));
}
result.set("result", node);
}
return ok(result);

View File

@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import models.Dataset;
import models.FlowJob;
import models.Metric;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@ -26,12 +27,12 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import play.Logger;
import play.Play;
import play.libs.F;
import play.libs.Json;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import play.libs.WS;
import java.util.*;
public class AdvSearchDAO extends AbstractMySQLOpenSourceDAO
{
@ -100,6 +101,14 @@ public class AdvSearchDAO extends AbstractMySQLOpenSourceDAO
"FROM flow_job j JOIN flow f on j.app_id = f.app_id AND j.flow_id = f.flow_id " +
"JOIN cfg_application a on j.app_id = a.app_id ";
public final static String ADV_SEARCH_METRIC = "SELECT SQL_CALC_FOUND_ROWS metric_id, " +
"metric_name, metric_description, dashboard_name, metric_group, metric_category, " +
"metric_sub_category, metric_level, metric_source_type, metric_source, " +
"metric_source_dataset_id, metric_ref_id_type, metric_ref_id, metric_type, metric_grain, " +
"metric_display_factor, metric_display_factor_sym, metric_good_direction, " +
"metric_formula, dimensions, owners, tags, urn, metric_url, wiki_url, scm_url, 0 as watch_id " +
"FROM dict_business_metric ";
public static List<String> getDatasetSources()
@ -193,6 +202,246 @@ public class AdvSearchDAO extends AbstractMySQLOpenSourceDAO
return getJdbcTemplate().queryForList(GET_JOB_NAMES, String.class);
}
public static ObjectNode elasticSearch(JsonNode searchOpt, int page, int size)
{
ObjectNode resultNode = Json.newObject();
Long count = 0L;
List<Dataset> pagedDatasets = new ArrayList<Dataset>();
ObjectNode queryNode = Json.newObject();
queryNode.put("from", (page-1)*size);
queryNode.put("size", size);
JsonNode searchNode = utils.Search.generateDatasetAdvSearchQueryString(searchOpt);
if (searchNode != null && searchNode.isContainerNode())
{
queryNode.put("query", searchNode);
}
F.Promise < WS.Response> responsePromise = WS.url(
Play.application().configuration().getString(
SearchDAO.ELASTICSEARCH_DATASET_URL_KEY)).post(queryNode);
JsonNode responseNode = responsePromise.get().asJson();
resultNode.put("page", page);
resultNode.put("category", "Datasets");
resultNode.put("itemsPerPage", size);
if (responseNode != null && responseNode.isContainerNode() && responseNode.has("hits")) {
JsonNode hitsNode = responseNode.get("hits");
if (hitsNode != null) {
if (hitsNode.has("total")) {
count = hitsNode.get("total").asLong();
}
if (hitsNode.has("hits")) {
JsonNode dataNode = hitsNode.get("hits");
if (dataNode != null && dataNode.isArray()) {
Iterator<JsonNode> arrayIterator = dataNode.elements();
if (arrayIterator != null) {
while (arrayIterator.hasNext()) {
JsonNode node = arrayIterator.next();
if (node.isContainerNode() && node.has("_id")) {
Dataset dataset = new Dataset();
dataset.id = node.get("_id").asLong();
if (node.has("_source")) {
JsonNode sourceNode = node.get("_source");
if (sourceNode != null) {
if (sourceNode.has("name")) {
dataset.name = sourceNode.get("name").asText();
}
if (sourceNode.has("source")) {
dataset.source = sourceNode.get("source").asText();
}
if (sourceNode.has("urn")) {
dataset.urn = sourceNode.get("urn").asText();
}
if (sourceNode.has("schema")) {
dataset.schema = sourceNode.get("schema").asText();
}
}
}
pagedDatasets.add(dataset);
}
}
}
}
}
}
}
resultNode.put("count", count);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedDatasets));
return resultNode;
}
public static ObjectNode elasticSearchMetric(JsonNode searchOpt, int page, int size)
{
ObjectNode resultNode = Json.newObject();
Long count = 0L;
List<Metric> pagedMetrics = new ArrayList<Metric>();
ObjectNode queryNode = Json.newObject();
queryNode.put("from", (page-1)*size);
queryNode.put("size", size);
JsonNode searchNode = utils.Search.generateMetricAdvSearchQueryString(searchOpt);
if (searchNode != null && searchNode.isContainerNode())
{
queryNode.put("query", searchNode);
}
F.Promise < WS.Response> responsePromise = WS.url(Play.application().configuration().getString(
SearchDAO.ELASTICSEARCH_METRIC_URL_KEY)).post(queryNode);
JsonNode responseNode = responsePromise.get().asJson();
resultNode.put("page", page);
resultNode.put("category", "Metrics");
resultNode.put("isMetrics", true);
resultNode.put("itemsPerPage", size);
if (responseNode != null && responseNode.isContainerNode() && responseNode.has("hits")) {
JsonNode hitsNode = responseNode.get("hits");
if (hitsNode != null) {
if (hitsNode.has("total")) {
count = hitsNode.get("total").asLong();
}
if (hitsNode.has("hits")) {
JsonNode dataNode = hitsNode.get("hits");
if (dataNode != null && dataNode.isArray()) {
Iterator<JsonNode> arrayIterator = dataNode.elements();
if (arrayIterator != null) {
while (arrayIterator.hasNext()) {
JsonNode node = arrayIterator.next();
if (node.isContainerNode() && node.has("_id")) {
Metric metric = new Metric();
metric.id = node.get("_id").asInt();
if (node.has("_source")) {
JsonNode sourceNode = node.get("_source");
if (sourceNode != null) {
if (sourceNode.has("metric_name")) {
metric.name = sourceNode.get("metric_name").asText();
}
if (sourceNode.has("metric_description")) {
metric.description = sourceNode.get("metric_description").asText();
}
if (sourceNode.has("dashboard_name")) {
metric.dashboardName = sourceNode.get("dashboard_name").asText();
}
if (sourceNode.has("metric_group")) {
metric.group = sourceNode.get("metric_group").asText();
}
if (sourceNode.has("metric_category")) {
metric.category = sourceNode.get("metric_category").asText();
}
if (sourceNode.has("urn")) {
metric.urn = sourceNode.get("urn").asText();
}
if (sourceNode.has("metric_source")) {
metric.source = sourceNode.get("metric_source").asText();
if (StringUtils.isBlank(metric.source))
{
metric.source = null;
}
}
metric.schema = sourceNode.toString();
}
}
pagedMetrics.add(metric);
}
}
}
}
}
}
}
resultNode.put("count", count);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedMetrics));
return resultNode;
}
public static ObjectNode elasticSearchFlowJobs(JsonNode searchOpt, int page, int size)
{
ObjectNode resultNode = Json.newObject();
Long count = 0L;
List<FlowJob> pagedFlows = new ArrayList<FlowJob>();
ObjectNode queryNode = Json.newObject();
queryNode.put("from", (page-1)*size);
queryNode.put("size", size);
JsonNode searchNode = utils.Search.generateFlowJobAdvSearchQueryString(searchOpt);
if (searchNode != null && searchNode.isContainerNode())
{
queryNode.put("query", searchNode);
}
F.Promise < WS.Response> responsePromise = WS.url(Play.application().configuration().getString(
SearchDAO.ELASTICSEARCH_FLOW_URL_KEY)).post(queryNode);
JsonNode responseNode = responsePromise.get().asJson();
resultNode.put("page", page);
resultNode.put("category", "Flows");
resultNode.put("isFlowJob", true);
resultNode.put("itemsPerPage", size);
if (responseNode != null && responseNode.isContainerNode() && responseNode.has("hits")) {
JsonNode hitsNode = responseNode.get("hits");
if (hitsNode != null) {
if (hitsNode.has("total")) {
count = hitsNode.get("total").asLong();
}
if (hitsNode.has("hits")) {
JsonNode dataNode = hitsNode.get("hits");
if (dataNode != null && dataNode.isArray()) {
Iterator<JsonNode> arrayIterator = dataNode.elements();
if (arrayIterator != null) {
while (arrayIterator.hasNext()) {
JsonNode node = arrayIterator.next();
if (node.isContainerNode() && node.has("_id")) {
FlowJob flowJob = new FlowJob();
if (node.has("_source")) {
JsonNode sourceNode = node.get("_source");
if (sourceNode != null) {
if (sourceNode.has("app_code")) {
flowJob.appCode = sourceNode.get("app_code").asText();
}
if (sourceNode.has("app_id")) {
flowJob.appId = sourceNode.get("app_id").asInt();
}
if (sourceNode.has("flow_id")) {
flowJob.flowId = sourceNode.get("flow_id").asLong();
}
if (sourceNode.has("flow_name")) {
flowJob.flowName = sourceNode.get("flow_name").asText();
flowJob.displayName = flowJob.flowName;
}
if (sourceNode.has("flow_path")) {
flowJob.flowPath = sourceNode.get("flow_path").asText();
}
if (sourceNode.has("flow_group")) {
flowJob.flowGroup = sourceNode.get("flow_group").asText();
}
flowJob.link = "#/flows/" + flowJob.appCode + "/" +
flowJob.flowGroup + "/" + Long.toString(flowJob.flowId) + "/page/1";
flowJob.path = flowJob.appCode + "/" + flowJob.flowPath;
flowJob.schema = sourceNode.toString();
}
}
pagedFlows.add(flowJob);
}
}
}
}
}
}
}
resultNode.put("count", count);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedFlows));
return resultNode;
}
public static ObjectNode search(JsonNode searchOpt, int page, int size)
{
ObjectNode resultNode = Json.newObject();
@ -1291,4 +1540,495 @@ public class AdvSearchDAO extends AbstractMySQLOpenSourceDAO
return resultNode;
}
public static ObjectNode searchMetrics(JsonNode searchOpt, int page, int size)
{
ObjectNode resultNode = Json.newObject();
int count = 0;
List<String> dashboardInList = new ArrayList<String>();
List<String> dashboardNotInList = new ArrayList<String>();
List<String> groupInList = new ArrayList<String>();
List<String> groupNotInList = new ArrayList<String>();
List<String> categoryInList = new ArrayList<String>();
List<String> categoryNotInList = new ArrayList<String>();
List<String> metricInList = new ArrayList<String>();
List<String> metricNotInList = new ArrayList<String>();
if (searchOpt != null && (searchOpt.isContainerNode()))
{
if (searchOpt.has("dashboard")) {
JsonNode dashboardNode = searchOpt.get("dashboard");
if (dashboardNode != null && dashboardNode.isContainerNode())
{
if (dashboardNode.has("in"))
{
JsonNode dashboardInNode = dashboardNode.get("in");
if (dashboardInNode != null)
{
String dashboardInStr = dashboardInNode.asText();
if (StringUtils.isNotBlank(dashboardInStr))
{
String[] dashboardInArray = dashboardInStr.split(",");
if (dashboardInArray != null)
{
for(String value : dashboardInArray)
{
if (StringUtils.isNotBlank(value))
{
dashboardInList.add(value.trim());
}
}
}
}
}
}
if (dashboardNode.has("not"))
{
JsonNode dashboardNotInNode = dashboardNode.get("not");
if (dashboardNotInNode != null)
{
String dashboardNotInStr = dashboardNotInNode.asText();
if (StringUtils.isNotBlank(dashboardNotInStr))
{
String[] dashboardNotInArray = dashboardNotInStr.split(",");
if (dashboardNotInArray != null)
{
for(String value : dashboardNotInArray)
{
if (StringUtils.isNotBlank(value))
{
dashboardNotInList.add(value.trim());
}
}
}
}
}
}
}
}
if (searchOpt.has("group")) {
JsonNode groupNode = searchOpt.get("group");
if (groupNode != null && groupNode.isContainerNode())
{
if (groupNode.has("in"))
{
JsonNode groupInNode = groupNode.get("in");
if (groupInNode != null)
{
String groupInStr = groupInNode.asText();
if (StringUtils.isNotBlank(groupInStr))
{
String[] groupInArray = groupInStr.split(",");
if (groupInArray != null)
{
for(String value : groupInArray)
{
if (StringUtils.isNotBlank(value))
{
groupInList.add(value.trim());
}
}
}
}
}
}
if (groupNode.has("not"))
{
JsonNode groupNotInNode = groupNode.get("not");
if (groupNotInNode != null)
{
String groupNotInStr = groupNotInNode.asText();
if (StringUtils.isNotBlank(groupNotInStr))
{
String[] groupNotInArray = groupNotInStr.split(",");
if (groupNotInArray != null)
{
for(String value : groupNotInArray)
{
if (StringUtils.isNotBlank(value))
{
groupNotInList.add(value.trim());
}
}
}
}
}
}
}
}
if (searchOpt.has("cat")) {
JsonNode categoryNode = searchOpt.get("cat");
if (categoryNode != null && categoryNode.isContainerNode())
{
if (categoryNode.has("in"))
{
JsonNode categoryInNode = categoryNode.get("in");
if (categoryInNode != null)
{
String categoryInStr = categoryInNode.asText();
if (StringUtils.isNotBlank(categoryInStr))
{
String[] categoryInArray = categoryInStr.split(",");
if (categoryInArray != null)
{
for(String value : categoryInArray)
{
if (StringUtils.isNotBlank(value))
{
categoryInList.add(value.trim());
}
}
}
}
}
}
if (categoryNode.has("not"))
{
JsonNode categoryNotInNode = categoryNode.get("not");
if (categoryNotInNode != null)
{
String categoryNotInStr = categoryNotInNode.asText();
if (StringUtils.isNotBlank(categoryNotInStr))
{
String[] categoryNotInArray = categoryNotInStr.split(",");
if (categoryNotInArray != null)
{
for(String value : categoryNotInArray)
{
if (StringUtils.isNotBlank(value))
{
categoryNotInList.add(value.trim());
}
}
}
}
}
}
}
}
if (searchOpt.has("metric")) {
JsonNode metricNode = searchOpt.get("metric");
if (metricNode != null && metricNode.isContainerNode())
{
if (metricNode.has("in"))
{
JsonNode metricInNode = metricNode.get("in");
if (metricInNode != null)
{
String metricInStr = metricInNode.asText();
if (StringUtils.isNotBlank(metricInStr))
{
String[] metricInArray = metricInStr.split(",");
if (metricInArray != null)
{
for(String value : metricInArray)
{
if (StringUtils.isNotBlank(value))
{
metricInList.add(value.trim());
}
}
}
}
}
}
if (metricNode.has("not"))
{
JsonNode metricNotInNode = metricNode.get("not");
if (metricNotInNode != null)
{
String metricNotInStr = metricNotInNode.asText();
if (StringUtils.isNotBlank(metricNotInStr))
{
String[] metricNotInArray = metricNotInStr.split(",");
if (metricNotInArray != null)
{
for(String value : metricNotInArray)
{
if (StringUtils.isNotBlank(value))
{
metricNotInList.add(value.trim());
}
}
}
}
}
}
}
}
boolean needAndKeyword = false;
final List<Metric> pagedMetrics = new ArrayList<Metric>();
final JdbcTemplate jdbcTemplate = getJdbcTemplate();
javax.sql.DataSource ds = jdbcTemplate.getDataSource();
DataSourceTransactionManager tm = new DataSourceTransactionManager(ds);
TransactionTemplate txTemplate = new TransactionTemplate(tm);
ObjectNode result;
String query = ADV_SEARCH_METRIC;
if (dashboardInList.size() > 0 || dashboardNotInList.size() > 0)
{
boolean dashboardNeedAndKeyword = false;
if (dashboardInList.size() > 0)
{
int indexForDashboardInList = 0;
for (String dashboard : dashboardInList)
{
if (indexForDashboardInList == 0)
{
query += "WHERE dashboard_name in ('" + dashboard + "'";
}
else
{
query += ", '" + dashboard + "'";
}
indexForDashboardInList++;
}
query += ") ";
dashboardNeedAndKeyword = true;
}
if (dashboardNotInList.size() > 0)
{
if (dashboardNeedAndKeyword)
{
query += " AND ";
}
else
{
query += " WHERE ";
}
int indexForDashboardNotInList = 0;
for (String dashboard : dashboardNotInList)
{
if (indexForDashboardNotInList == 0)
{
query += "dashboard_name not in ('" + dashboard + "'";
}
else
{
query += ", '" + dashboard + "'";
}
indexForDashboardNotInList++;
}
query += ") ";
}
needAndKeyword = true;
}
if (groupInList.size() > 0 || groupNotInList.size() > 0)
{
if (needAndKeyword)
{
query += " AND ";
}
else
{
query += " WHERE ";
}
query += "( ";
boolean groupNeedAndKeyword = false;
if (groupInList.size() > 0)
{
query += "( ";
int indexForGroupInList = 0;
for (String group : groupInList)
{
if (indexForGroupInList == 0)
{
query += "metric_group LIKE '%" + group + "%'";
}
else
{
query += " or metric_group LIKE '%" + group + "%'";
}
indexForGroupInList++;
}
query += ") ";
groupNeedAndKeyword = true;
}
if (groupNotInList.size() > 0)
{
if (groupNeedAndKeyword)
{
query += " AND ";
}
query += "( ";
int indexForGroupNotInList = 0;
for (String group : groupNotInList)
{
if (indexForGroupNotInList == 0)
{
query += "metric_group NOT LIKE '%" + group + "%'";
}
else
{
query += " and metric_group NOT LIKE '%" + group + "%'";
}
indexForGroupNotInList++;
}
query += ") ";
}
query += ") ";
needAndKeyword = true;
}
if (categoryInList.size() > 0 || categoryNotInList.size() > 0)
{
if (needAndKeyword)
{
query += " AND ";
}
else
{
query += " WHERE ";
}
query += "( ";
boolean categoryNeedAndKeyword = false;
if (categoryInList.size() > 0)
{
int indexForCategoryInList = 0;
query += "( ";
for (String category : categoryInList)
{
if (indexForCategoryInList == 0)
{
query += "metric_category LIKE '%" + category + "%'";
}
else
{
query += " or metric_category LIKE '%" + category + "%'";
}
indexForCategoryInList++;
}
query += ") ";
categoryNeedAndKeyword = true;
}
if (categoryNotInList.size() > 0)
{
if (categoryNeedAndKeyword)
{
query += " AND ";
}
query += "( ";
int indexForCategoryNotInList = 0;
for (String category : categoryNotInList)
{
if (indexForCategoryNotInList == 0)
{
query += "metric_category NOT LIKE '%" + category + "%'";
}
else
{
query += " and metric_category NOT LIKE '%" + category + "%'";
}
indexForCategoryNotInList++;
}
query += ") ";
}
query += ") ";
needAndKeyword = true;
}
if (metricInList.size() > 0 || metricNotInList.size() > 0)
{
if (needAndKeyword)
{
query += " AND ";
}
else
{
query += " WHERE ";
}
query += "( ";
boolean metricNeedAndKeyword = false;
if (metricInList.size() > 0)
{
int indexForMetricInList = 0;
query += " ( ";
for (String metric : metricInList)
{
if (indexForMetricInList == 0)
{
query += "metric_name LIKE '%" + metric + "%'";
}
else
{
query += " or metric_name LIKE '%" + metric + "%'";
}
indexForMetricInList++;
}
query += ") ";
metricNeedAndKeyword = true;
}
if (metricNotInList.size() > 0)
{
if (metricNeedAndKeyword)
{
query += " AND ";
}
query += "( ";
int indexForMetricNotInList = 0;
for (String metric : metricNotInList)
{
if (indexForMetricNotInList == 0)
{
query += "metric_name NOT LIKE '%" + metric + "%'";
}
else
{
query += " and metric_name NOT LIKE '%" + metric + "%'";
}
indexForMetricNotInList++;
}
query += ") ";
}
query += " )";
}
query += " LIMIT " + (page-1)*size + ", " + size;
final String queryString = query;
result = txTemplate.execute(new TransactionCallback<ObjectNode>()
{
public ObjectNode doInTransaction(TransactionStatus status)
{
List<Metric> pagedMetrics = jdbcTemplate.query(queryString, new MetricRowMapper());
long count = 0;
try {
count = jdbcTemplate.queryForObject(
"SELECT FOUND_ROWS()",
Long.class);
}
catch(EmptyResultDataAccessException e)
{
Logger.error("Exception = " + e.getMessage());
}
ObjectNode resultNode = Json.newObject();
resultNode.put("count", count);
resultNode.put("page", page);
resultNode.put("isMetrics", true);
resultNode.put("itemsPerPage", size);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedMetrics));
return resultNode;
}
});
return result;
}
resultNode.put("count", 0);
resultNode.put("page", page);
resultNode.put("itemsPerPage", size);
resultNode.put("totalPages", 0);
resultNode.set("data", Json.toJson(""));
return resultNode;
}
}

View File

@ -13,13 +13,11 @@
*/
package dao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.EmptyResultDataAccessException;
@ -29,12 +27,23 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import play.Logger;
import play.Play;
import play.libs.F;
import play.libs.Json;
import play.cache.Cache;
import models.*;
import play.libs.WS;
public class SearchDAO extends AbstractMySQLOpenSourceDAO
{
public static String ELASTICSEARCH_DATASET_URL_KEY = "elasticsearch.dataset.url";
public static String ELASTICSEARCH_METRIC_URL_KEY = "elasticsearch.metric.url";
public static String ELASTICSEARCH_FLOW_URL_KEY = "elasticsearch.flow.url";
public static String WHEREHOWS_SEARCH_ENGINE__KEY = "search.engine";
public final static String SEARCH_DATASET_WITH_PAGINATION = "SELECT SQL_CALC_FOUND_ROWS " +
"id, `name`, `schema`, `source`, `urn`, FROM_UNIXTIME(source_modified_time) as modified, " +
"rank_01 + rank_02 + rank_03 + rank_04 + rank_05 + rank_06 + rank_07 + rank_08 + rank_09 as rank " +
@ -169,6 +178,324 @@ public class SearchDAO extends AbstractMySQLOpenSourceDAO
return cachedAutoCompleteList;
}
public static JsonNode elasticSearchDatasetByKeyword(
String category,
String keywords,
String source,
int page,
int size)
{
ObjectNode queryNode = Json.newObject();
queryNode.put("from", (page-1)*size);
queryNode.put("size", size);
JsonNode responseNode = null;
ObjectNode keywordNode = null;
try
{
keywordNode = utils.Search.generateElasticSearchQueryString(category, source, keywords);
}
catch(Exception e)
{
Logger.error("Elastic search dataset input query is not JSON format. Error message :" + e.getMessage());
}
if (keywordNode != null)
{
queryNode.put("query", keywordNode);
F.Promise < WS.Response> responsePromise = WS.url(Play.application().configuration().getString(
SearchDAO.ELASTICSEARCH_DATASET_URL_KEY)).post(queryNode);
responseNode = responsePromise.get().asJson();
}
ObjectNode resultNode = Json.newObject();
Long count = 0L;
List<Dataset> pagedDatasets = new ArrayList<Dataset>();
resultNode.put("page", page);
resultNode.put("category", category);
resultNode.put("source", source);
resultNode.put("itemsPerPage", size);
resultNode.put("keywords", keywords);
if (responseNode != null && responseNode.isContainerNode() && responseNode.has("hits"))
{
JsonNode hitsNode = responseNode.get("hits");
if (hitsNode != null)
{
if (hitsNode.has("total"))
{
count = hitsNode.get("total").asLong();
}
if (hitsNode.has("hits"))
{
JsonNode dataNode = hitsNode.get("hits");
if (dataNode != null && dataNode.isArray())
{
Iterator<JsonNode> arrayIterator = dataNode.elements();
if (arrayIterator != null)
{
while (arrayIterator.hasNext())
{
JsonNode node = arrayIterator.next();
if (node.isContainerNode() && node.has("_id"))
{
Dataset dataset = new Dataset();
dataset.id = node.get("_id").asLong();
if (node.has("_source"))
{
JsonNode sourceNode = node.get("_source");
if (sourceNode != null)
{
if (sourceNode.has("name"))
{
dataset.name = sourceNode.get("name").asText();
}
if (sourceNode.has("source"))
{
dataset.source = sourceNode.get("source").asText();
}
if (sourceNode.has("urn"))
{
dataset.urn = sourceNode.get("urn").asText();
}
if (sourceNode.has("schema"))
{
dataset.schema = sourceNode.get("schema").asText();
}
}
}
pagedDatasets.add(dataset);
}
}
}
}
}
}
}
resultNode.put("count", count);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedDatasets));
return resultNode;
}
public static JsonNode elasticSearchMetricByKeyword(
String category,
String keywords,
int page,
int size)
{
ObjectNode queryNode = Json.newObject();
queryNode.put("from", (page-1)*size);
queryNode.put("size", size);
JsonNode responseNode = null;
ObjectNode keywordNode = null;
try
{
keywordNode = utils.Search.generateElasticSearchQueryString(category, null, keywords);
}
catch(Exception e)
{
Logger.error("Elastic search metric input query is not JSON format. Error message :" + e.getMessage());
}
if (keywordNode != null)
{
queryNode.put("query", keywordNode);
F.Promise < WS.Response> responsePromise = WS.url(Play.application().configuration().getString(
SearchDAO.ELASTICSEARCH_METRIC_URL_KEY)).post(queryNode);
responseNode = responsePromise.get().asJson();
}
ObjectNode resultNode = Json.newObject();
Long count = 0L;
List<Metric> pagedMetrics = new ArrayList<Metric>();
resultNode.put("page", page);
resultNode.put("category", category);
resultNode.put("isMetrics", true);
resultNode.put("itemsPerPage", size);
resultNode.put("keywords", keywords);
if (responseNode != null && responseNode.isContainerNode() && responseNode.has("hits"))
{
JsonNode hitsNode = responseNode.get("hits");
if (hitsNode != null)
{
if (hitsNode.has("total"))
{
count = hitsNode.get("total").asLong();
}
if (hitsNode.has("hits"))
{
JsonNode dataNode = hitsNode.get("hits");
if (dataNode != null && dataNode.isArray())
{
Iterator<JsonNode> arrayIterator = dataNode.elements();
if (arrayIterator != null)
{
while (arrayIterator.hasNext())
{
JsonNode node = arrayIterator.next();
if (node.isContainerNode() && node.has("_id"))
{
Metric metric = new Metric();
metric.id = node.get("_id").asInt();
if (node.has("_source")) {
JsonNode sourceNode = node.get("_source");
if (sourceNode != null) {
if (sourceNode.has("metric_name")) {
metric.name = sourceNode.get("metric_name").asText();
}
if (sourceNode.has("metric_description")) {
metric.description = sourceNode.get("metric_description").asText();
}
if (sourceNode.has("dashboard_name")) {
metric.dashboardName = sourceNode.get("dashboard_name").asText();
}
if (sourceNode.has("metric_group")) {
metric.group = sourceNode.get("metric_group").asText();
}
if (sourceNode.has("metric_category")) {
metric.category = sourceNode.get("metric_category").asText();
}
if (sourceNode.has("urn")) {
metric.urn = sourceNode.get("urn").asText();
}
if (sourceNode.has("metric_source")) {
metric.source = sourceNode.get("metric_source").asText();
if (StringUtils.isBlank(metric.source))
{
metric.source = null;
}
}
metric.schema = sourceNode.toString();
}
}
pagedMetrics.add(metric);
}
}
}
}
}
}
}
resultNode.put("count", count);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedMetrics));
return resultNode;
}
public static JsonNode elasticSearchFlowByKeyword(
String category,
String keywords,
int page,
int size)
{
ObjectNode queryNode = Json.newObject();
queryNode.put("from", (page-1)*size);
queryNode.put("size", size);
JsonNode searchOpt = null;
JsonNode responseNode = null;
ObjectNode keywordNode = null;
try
{
keywordNode = utils.Search.generateElasticSearchQueryString(category, null, keywords);
}
catch(Exception e)
{
Logger.error("Elastic search flow input query is not JSON format. Error message :" + e.getMessage());
}
if (keywordNode != null)
{
queryNode.put("query", keywordNode);
F.Promise < WS.Response> responsePromise = WS.url(Play.application().configuration().getString(
SearchDAO.ELASTICSEARCH_FLOW_URL_KEY)).post(queryNode);
responseNode = responsePromise.get().asJson();
}
ObjectNode resultNode = Json.newObject();
Long count = 0L;
List<FlowJob> pagedFlowJobs = new ArrayList<FlowJob>();
resultNode.put("page", page);
resultNode.put("category", category);
resultNode.put("isFlowJob", true);
resultNode.put("itemsPerPage", size);
resultNode.put("keywords", keywords);
if (responseNode != null && responseNode.isContainerNode() && responseNode.has("hits"))
{
JsonNode hitsNode = responseNode.get("hits");
if (hitsNode != null)
{
if (hitsNode.has("total"))
{
count = hitsNode.get("total").asLong();
}
if (hitsNode.has("hits"))
{
JsonNode dataNode = hitsNode.get("hits");
if (dataNode != null && dataNode.isArray())
{
Iterator<JsonNode> arrayIterator = dataNode.elements();
if (arrayIterator != null)
{
while (arrayIterator.hasNext())
{
JsonNode node = arrayIterator.next();
if (node.isContainerNode() && node.has("_id"))
{
FlowJob flowJob = new FlowJob();
if (node.has("_source")) {
JsonNode sourceNode = node.get("_source");
if (sourceNode != null) {
if (sourceNode.has("app_code")) {
flowJob.appCode = sourceNode.get("app_code").asText();
}
if (sourceNode.has("app_id")) {
flowJob.appId = sourceNode.get("app_id").asInt();
}
if (sourceNode.has("flow_id")) {
flowJob.flowId = sourceNode.get("flow_id").asLong();
}
if (sourceNode.has("flow_name")) {
flowJob.flowName = sourceNode.get("flow_name").asText();
flowJob.displayName = flowJob.flowName;
}
if (sourceNode.has("flow_path")) {
flowJob.flowPath = sourceNode.get("flow_path").asText();
}
if (sourceNode.has("flow_group")) {
flowJob.flowGroup = sourceNode.get("flow_group").asText();
}
flowJob.link = "#/flows/" + flowJob.appCode + "/" +
flowJob.flowGroup + "/" + Long.toString(flowJob.flowId) + "/page/1";
flowJob.path = flowJob.appCode + "/" + flowJob.flowPath;
flowJob.schema = sourceNode.toString();
}
}
pagedFlowJobs.add(flowJob);
}
}
}
}
}
}
}
resultNode.put("count", count);
resultNode.put("totalPages", (int)Math.ceil(count/((double)size)));
resultNode.set("data", Json.toJson(pagedFlowJobs));
return resultNode;
}
public static ObjectNode getPagedDatasetByKeyword(String category, String keyword, String source, int page, int size)
{
List<Dataset> pagedDatasets = new ArrayList<Dataset>();

View File

@ -28,4 +28,5 @@ public class FlowJob {
public String path;
public Integer appId;
public Long flowId;
public String schema;
}

1194
web/app/utils/Search.java Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1198,6 +1198,7 @@
<td class="col-xs-12">
<div class="dataset-name">
<td class="dataset-info">
<i class="fa fa-random"></i>
<a href="{{flowJob.link}}">
{{flowJob.displayName}}
</a>
@ -1206,6 +1207,9 @@
{{{ flowJob.path }}}
</p>
<p>source: {{{ flowJob.appCode }}}</p>
<div class="schematext" style="margin-top:5px;margin-bottom: 10px;">
{{{ flowJob.schema }}}
</div>
</div>
</td>
</tr>
@ -1221,10 +1225,12 @@
<div class="dataset-name">
<td class="dataset-info">
{{#if isMetric}}
<i class="fa fa-plus-square-o"></i>
{{#link-to 'metric' dataset}}
{{{dataset.name}}}
{{/link-to}}
{{else}}
<i class="fa fa-database"></i>
{{#link-to 'dataset' dataset}}
{{{dataset.name}}}
{{/link-to}}
@ -1233,7 +1239,11 @@
<p>
{{{ dataset.urn }}}
</p>
<p>source: {{{ dataset.source }}}</p>
{{#if dataset.source}}
<p>source: {{{ dataset.source }}}</p>
{{else}}
<p>source: Metric</p>
{{/if}}
<div class="schematext" style="margin-top:5px;margin-bottom: 10px;">
{{{ dataset.schema }}}
</div>
@ -1302,6 +1312,7 @@
<td class="col-xs-12">
<div class="dataset-name">
<td class="dataset-info">
<i class="fa fa-random"></i>
<a href="{{flowJob.link}}">
{{flowJob.displayName}}
</a>
@ -1310,6 +1321,9 @@
{{{ flowJob.path }}}
</p>
<p>source: {{{ flowJob.appCode }}}</p>
<div class="schematext" style="margin-top:5px;margin-bottom: 10px;">
{{{ flowJob.schema }}}
</div>
</div>
</td>
</tr>
@ -1324,6 +1338,7 @@
<td class="col-xs-12">
<div class="dataset-name">
<td class="dataset-info">
<i class="fa fa-database"></i>
{{#link-to 'dataset' dataset}}
{{{dataset.name}}}
{{/link-to}}

View File

@ -90,20 +90,55 @@
</li>
</ul>
<form class="navbar-form navbar-left" role="search">
<div class="input-group">
<input id="searchInput"
type="text"
class="form-control input-sm keyword-search"
placeholder="Enter Keywords..."
/>
<span class="input-group-btn">
<button id="searchBtn"
type="button"
class="btn btn-sm btn-primary"
>
<i class="fa fa-search"></i>
</button>
</span>
<div class="row">
<div class="btn-group" role="group">
<button style="height: 30px;margin-right:-4px;"
type="button"
data-toggle="dropdown"
aria-expanded="false">
<i id="categoryIcon" class="fa fa-database"></i>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<!--
<li class="active">
<a href="#" class="searchCategory">All</a>
</li>
-->
<li id="categoryDatasets" class="active">
<a href="#" class="searchCategory">Datasets</a>
</li>
<li id="categoryComments" >
<a href="#" class="searchCategory">Comments</a>
</li>
<!--
<li id="categoryMetrics" >
<a href="#" class="searchCategory">Metrics</a>
</li>
-->
<li id="categoryFlows" >
<a href="#" class="searchCategory">Flows</a>
</li>
<li id="categoryJobs" >
<a href="#" class="searchCategory">Jobs</a>
</li>
</ul>
</div>
<div class="input-group">
<input id="searchInput"
type="text"
class="form-control input-sm keyword-search"
placeholder="Enter Keywords..."
/>
<span class="input-group-btn">
<button id="searchBtn"
type="button"
class="btn btn-sm btn-primary"
>
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
</form>
<div class="nav nabar-nav navbar-left">

View File

@ -93,6 +93,101 @@ var convertQueryStringToObject = function() {
return queryString;
}
function resetCategoryActiveFlag(category)
{
$('#categoryDatasets').removeClass('active');
$('#categoryComments').removeClass('active');
$('#categoryMetrics').removeClass('active');
$('#categoryFlows').removeClass('active');
$('#categoryJobs').removeClass('active');
if (category.toLowerCase() == 'datasets')
{
$('#categoryDatasets').addClass('active');
}
else if (category.toLowerCase() == 'comments')
{
$('#categoryComments').addClass('active');
}
else if (category.toLowerCase() == 'metrics')
{
$('#categoryMetrics').addClass('active');
}
else if (category.toLowerCase() == 'flows')
{
$('#categoryFlows').addClass('active');
}
else if (category.toLowerCase() == 'jobs')
{
$('#categoryJobs').addClass('active');
}
currentCategory = category;
}
function updateSearchCategories(category)
{
if (category.toLowerCase() == 'all')
{
$('#categoryIcon').removeClass('fa fa-list');
$('#categoryIcon').removeClass('fa fa-database');
$('#categoryIcon').removeClass('fa fa-comment');
$('#categoryIcon').removeClass('fa fa-random');
$('#categoryIcon').removeClass('fa fa-plus-square-o');
$('#categoryIcon').removeClass('fa fa-file-o');
$('#categoryIcon').addClass('fa fa-list');
}
else if (category.toLowerCase() == 'datasets')
{
$('#categoryIcon').removeClass('fa fa-list');
$('#categoryIcon').removeClass('fa fa-database');
$('#categoryIcon').removeClass('fa fa-comment');
$('#categoryIcon').removeClass('fa fa-random');
$('#categoryIcon').removeClass('fa fa-plus-square-o');
$('#categoryIcon').removeClass('fa fa-file-o');
$('#categoryIcon').addClass('fa fa-database');
}
else if (category.toLowerCase() == 'comments')
{
$('#categoryIcon').removeClass('fa fa-list');
$('#categoryIcon').removeClass('fa fa-database');
$('#categoryIcon').removeClass('fa fa-comment');
$('#categoryIcon').removeClass('fa fa-random');
$('#categoryIcon').removeClass('fa fa-plus-square-o');
$('#categoryIcon').removeClass('fa fa-file-o');
$('#categoryIcon').addClass('fa fa-comment');
}
else if (category.toLowerCase() == 'metrics')
{
$('#categoryIcon').removeClass('fa fa-list');
$('#categoryIcon').removeClass('fa fa-database');
$('#categoryIcon').removeClass('fa fa-comment');
$('#categoryIcon').removeClass('fa fa-random');
$('#categoryIcon').removeClass('fa fa-plus-square-o');
$('#categoryIcon').removeClass('fa fa-file-o');
$('#categoryIcon').addClass('fa fa-plus-square-o');
}
else if (category.toLowerCase() == 'flows')
{
$('#categoryIcon').removeClass('fa fa-list');
$('#categoryIcon').removeClass('fa fa-database');
$('#categoryIcon').removeClass('fa fa-comment');
$('#categoryIcon').removeClass('fa fa-random');
$('#categoryIcon').removeClass('fa fa-plus-square-o');
$('#categoryIcon').removeClass('fa fa-file-o');
$('#categoryIcon').addClass('fa fa-random');
}
else if (category.toLowerCase() == 'jobs')
{
$('#categoryIcon').removeClass('fa fa-list');
$('#categoryIcon').removeClass('fa fa-database');
$('#categoryIcon').removeClass('fa fa-comment');
$('#categoryIcon').removeClass('fa fa-random');
$('#categoryIcon').removeClass('fa fa-plus-square-o');
$('#categoryIcon').removeClass('fa fa-file-o');
$('#categoryIcon').addClass('fa fa-file-o');
}
resetCategoryActiveFlag(category);
}
String.prototype.toProperCase = function(){
return this.replace(/\w\S*/g, function(txt){
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()

View File

@ -20,8 +20,11 @@ function highlightResults(result, index, keyword)
var newContent = content.replace(query, "<b>$1</b>");
result[index].schema = newContent;
var urn = result[index].urn;
var newUrn = urn.replace(query, "<b>$1</b>");
result[index].urn = newUrn;
if (urn)
{
var newUrn = urn.replace(query, "<b>$1</b>");
result[index].urn = newUrn;
}
};
App.SearchRoute = Ember.Route.extend({
@ -65,6 +68,9 @@ App.SearchRoute = Ember.Route.extend({
$.get(url, function(data) {
if (data && data.status == "ok") {
var result = data.result;
var keywords = result.keywords;
window.g_currentCategory = result.category;
updateSearchCategories(result.category);
for(var index = 0; index < result.data.length; index++) {
var schema = result.data[index].schema;
if (schema) {

View File

@ -1,6 +1,10 @@
(function ($) {
(function (window, $) {
$('#advsearchtabs a:first').tab("show");
$('#datasetAdvSearchLink').addClass("active");
String.prototype.replaceAll = function(target, replacement) {
return this.split(target).join(replacement);
};
window.g_currentCategory = 'Datasets';
function renderAdvSearchDatasetSources(parent, sources)
{
if ((!parent) || (!sources) || sources.length == 0)
@ -72,7 +76,21 @@
parent.append(content);
}
var datasetSourcesUrl = '/api/v1/advsearch/sources';
$(".searchCategory").click(function(e){
var objs = $(".searchCategory");
if (objs)
{
$.each(objs, function( index, value ) {
$(objs[index]).parent().removeClass("active");
});
}
window.g_currentCategory = e.target.text;
updateSearchCategories(e.target.text);
//$(e.target).parent().addClass( "active" );
e.preventDefault();
});
var datasetSourcesUrl = '/api/v1/advsearch/sources';
$.get(datasetSourcesUrl, function(data) {
if (data && data.status == "ok")
{
@ -87,32 +105,25 @@
}
});
$("#searchInput").on( "keydown", function(event) {
if(event.which == 13)
{
event.preventDefault();
var inputObj = $('#searchInput');
if (inputObj) {
var keyword = inputObj.val();
if (keyword) {
window.location = '/#/search?keywords=' + btoa(keyword) +
'&category=Datasets&source=default&page=1';
}
}
}
});
$.get('/api/v1/autocomplete/search', function(data){
$('#searchInput').autocomplete({
source: function(request, response) {
var result = [];
if (data && data.source && request.term)
{
result = sortAutocompleteResult(data.source, request.term);
}
return response(result);
}
});
source: function( req, res ) {
var results = $.ui.autocomplete.filter(data.source, extractLast( req.term ));
res(results.slice(0,maxReturnedResults));
},
focus: function() {
return false;
},
select: function( event, ui ) {
var terms = split( this.value );
terms.pop();
terms.push( ui.item.value );
terms.push( "" );
this.value = terms.join( ", " );
return false;
}
});
});
@ -296,7 +307,7 @@
if (keyword)
{
window.location = '/#/search?keywords=' + btoa(keyword) +
'&category=Datasets&source=default&page=1';
'&category=' + window.g_currentCategory + '&source=default&page=1'
}
}
});
@ -577,4 +588,4 @@
}
});
})(jQuery)
})(window, jQuery)

View File

@ -669,4 +669,8 @@ div.commentsArea td, div.commentsArea th, div.commentsArea table{
.wh-clickable-icon {
cursor: pointer;
}
}
.keyword-search {
min-width: 500px;
}