diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/SearchResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/SearchResource.java index 18a39fd02418f3f65454d7f481243d04e44c02d0..e2e15af764bdc4cdd1787eef7d0babd4ed93a83b 100644 --- a/vipra-backend/src/main/java/de/vipra/rest/resource/SearchResource.java +++ b/vipra-backend/src/main/java/de/vipra/rest/resource/SearchResource.java @@ -19,7 +19,9 @@ import javax.ws.rs.core.Response; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.query.RangeQueryBuilder; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; @@ -46,7 +48,8 @@ public class SearchResource { @GET @Produces(MediaType.APPLICATION_JSON) public Response doSearch(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") Integer skip, @QueryParam("limit") Integer limit, - @QueryParam("fields") final String fields, @QueryParam("query") final String query) { + @QueryParam("fields") final String fields, @QueryParam("query") final String query, @QueryParam("long") final Long fromDate, + @QueryParam("to") final Long toDate) { final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>(); if (skip == null || skip < 0) @@ -63,11 +66,20 @@ public class SearchResource { indexName = topicModel + "-articles"; SearchResponse response = null; + + QueryBuilder qb = QueryBuilders.multiMatchQuery(query, "topics^" + Constants.ES_BOOST_TOPICS, "title^" + Constants.ES_BOOST_TITLES, "_all"); + + if (fromDate != null || toDate != null) { + final RangeQueryBuilder rqb = QueryBuilders.rangeQuery("date"); + if (fromDate != null) + rqb.from(fromDate); + if (toDate != null) + rqb.to(toDate); + qb = QueryBuilders.boolQuery().must(qb).must(rqb); + } + try { - response = client.prepareSearch(indexName) - .setQuery( - QueryBuilders.multiMatchQuery(query, "topics^" + Constants.ES_BOOST_TOPICS, "title^" + Constants.ES_BOOST_TITLES, "_all")) - .setFrom(skip).setSize(limit).execute().actionGet(); + response = client.prepareSearch(indexName).setQuery(qb).setFrom(skip).setSize(limit).execute().actionGet(); } catch (final Exception e) { e.printStackTrace(); res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage())); diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java index c6220dcc38b1440be8d4dd498fe3b7ddd6b64d88..7cda33e4e848a9ba6a410428a81259459e743682 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java @@ -6,7 +6,6 @@ import java.io.FileReader; import java.io.FilenameFilter; import java.io.IOException; import java.util.ArrayList; -import java.util.EnumSet; import java.util.List; import org.bson.types.ObjectId; @@ -146,31 +145,37 @@ public class ImportCommand implements Command { private void importArticle(final JSONObject object) { final ArticleFull article = articleFromJSON(object); - if (EnumSet.of(ProcessorMode.ENTITIES, ProcessorMode.TEXT_WITH_ENTITIES).contains(modelConfig.getProcessorMode())) { - try { - final SpotlightResponse spotlightResponse = spotlightAnalyzer.analyze(article.getText()); - - final List<TextEntity> textEntities = new ArrayList<>(spotlightResponse.getResources().size()); - final StringBuilder sb = new StringBuilder(); + try { - for (final SpotlightResource sr : spotlightResponse.getResources()) { - textEntities.add(new TextEntity(sr.getSurfaceForm(), sr.getUri())); + String text = article.getText(); - for (final String type : sr.getTypes()) { - final String[] parts = type.split(":"); - sb.append(" ").append(parts[parts.length - 1]); + if (spotlightAnalyzer != null) { + // extract entities + final SpotlightResponse spotlightResponse = spotlightAnalyzer.analyze(article.getText()); + final List<TextEntity> textEntities = spotlightResponse.getEntities(); + article.setEntities(textEntities); + + // replace/append text with entities in mixed/entities mode + if (modelConfig.getProcessorMode() == ProcessorMode.ENTITIES || modelConfig.getProcessorMode() == ProcessorMode.TEXT_WITH_ENTITIES) { + final StringBuilder sb = new StringBuilder(); + for (final SpotlightResource sr : spotlightResponse.getResources()) { + sb.append(" ").append(sr.getSurfaceForm()); + + for (final String type : sr.getTypes()) { + final String[] parts = type.split(":"); + sb.append(" ").append(parts[parts.length - 1]); + } } - } - // TODO do sth with this - } catch (final IOException e) { - ConsoleUtils.error("could not analyze text with spotlight: " + e.getMessage()); + if (modelConfig.getProcessorMode() == ProcessorMode.ENTITIES) + text = sb.toString().trim(); + else + text += " " + sb.toString(); + } } - } - try { // preprocess text - final ProcessedText processedText = processor.process(modelConfig, article.getText()); + final ProcessedText processedText = processor.process(modelConfig, text); if (processedText.getReducedWordCount() < modelConfig.getDocumentMinimumLength()) { ConsoleUtils.info(" skipped \"" + object.get("title")); @@ -198,6 +203,8 @@ public class ImportCommand implements Command { ConsoleUtils.error("could not save processed article in the database '" + article.getTitle() + "'"); } catch (final FilebaseException e) { ConsoleUtils.error("could not save processed article in the filebase '" + article.getTitle() + "'"); + } catch (IOException e) { + ConsoleUtils.error("io error"); } } @@ -240,7 +247,8 @@ public class ImportCommand implements Command { private void importForModel(final TopicModelConfig modelConfig) throws java.text.ParseException, IOException, ConfigException, ParseException, InterruptedException, DatabaseException { this.modelConfig = modelConfig; - if (this.modelConfig.getProcessorMode() == ProcessorMode.ENTITIES || this.modelConfig.getProcessorMode() == ProcessorMode.TEXT_WITH_ENTITIES) + + if (config.getSpotlightUrl() != null) spotlightAnalyzer = new SpotlightAnalyzer(modelConfig); buffer = new ArticleBuffer(dbArticles); diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/text/SpotlightResponse.java b/vipra-cmd/src/main/java/de/vipra/cmd/text/SpotlightResponse.java index 0cb0ce0c6c8321336c8c7c8ce24ec0ccc909d780..30832f4464445ffaf3a2fe642e718a619a35b5cf 100644 --- a/vipra-cmd/src/main/java/de/vipra/cmd/text/SpotlightResponse.java +++ b/vipra-cmd/src/main/java/de/vipra/cmd/text/SpotlightResponse.java @@ -1,10 +1,16 @@ package de.vipra.cmd.text; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import de.vipra.util.model.TextEntity; + @JsonIgnoreProperties(ignoreUnknown = true) public class SpotlightResponse { @@ -19,4 +25,15 @@ public class SpotlightResponse { this.resources = resources; } + public List<TextEntity> getEntities() { + final Set<TextEntity> textEntities = new HashSet<>(resources.size()); + for (SpotlightResource resource : resources) { + textEntities.add(new TextEntity(resource.getSurfaceForm(), resource.getUri())); + // TODO add types to entities? + } + final List<TextEntity> textEntitiesList = new ArrayList<>(textEntities); + Collections.sort(textEntitiesList); + return textEntitiesList; + } + } diff --git a/vipra-ui/app/html/index.html b/vipra-ui/app/html/index.html index 3eddb62d5e52b35d4990a1883e91cf2b4f9ac96b..30b090043852c409147f3f77e8a9a3a2bfca0c7a 100644 --- a/vipra-ui/app/html/index.html +++ b/vipra-ui/app/html/index.html @@ -27,11 +27,36 @@ </ul> </div> </div> - <div class="row row-spaced"> + <div class="row row-spaced search-row"> <div class="col-md-12"> - <div class="form-group has-feedback"> - <input type="text" class="form-control input-lg" placeholder="Search..." ng-model="search" ng-model-options="{debounce:500}" id="searchBox"> - <i class="form-control-feedback glyphicon glyphicon-search text-muted"></i> + <div class="input-group"> + <div class="form-group has-feedback"> + <input type="text" class="form-control input-lg" placeholder="Search..." ng-model="search" ng-model-options="{debounce:500}" id="searchBox"> + <i class="form-control-feedback glyphicon glyphicon-search text-muted"></i> + </div> + <span class="input-group-btn"> + <button class="btn btn-default btn-lg" type="button" title="Advanced" ng-click="advancedSearch=!advancedSearch"><i class="fa fa-chevron-down text-muted"></i></button> + </span> + </div> + </div> + </div> + <div class="row row-spaced" ng-show="advancedSearch"> + <div class="col-md-6 form-horizontal"> + <label for="advFromDate" class="col-sm-2 control-label">From</label> + <div class="input-group date col-sm-10" id="advFromDate" bs-datetimepicker ng-model="rootModels.advFromDate"> + <input type="text" class="form-control"> + <span class="input-group-addon"> + <span class="glyphicon glyphicon-calendar"></span> + </span> + </div> + </div> + <div class="col-md-6 form-horizontal"> + <label for="advToDate" class="col-sm-2 control-label">To</label> + <div class="input-group date col-sm-10" id="advToDate" bs-datetimepicker ng-model="rootModels.advToDate"> + <input type="text" class="form-control"> + <span class="input-group-addon"> + <span class="glyphicon glyphicon-calendar"></span> + </span> </div> </div> </div> diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index 92b9c03c8acd3003408ff37b0829dfc52643d8e4..b24be9ae3198005b8f1672faa41accc79d69941f 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -17,6 +17,19 @@ search: null }; + var prevTopicModelLoading = false; + if(localStorage.tm) { + prevTopicModelLoading = true + TopicModelFactory.get({ + id: localStorage.tm + }, function(data) { + $scope.rootModels.topicModel = data; + prevTopicModelLoading = false; + }, function() { + prevTopicModelLoading = false; + }) + } + $scope.queryTopicModels = function() { TopicModelFactory.query({ fields: '_all' @@ -26,6 +39,8 @@ }; $scope.chooseTopicModel = function() { + if(prevTopicModelLoading) + return; $scope.queryTopicModels(); $scope.rootModels.topicModelModalOpen = true; $('#topicModelModal').modal(); @@ -38,6 +53,7 @@ $scope.changeTopicModel = function(topicModel) { $scope.rootModels.topicModel = topicModel; $('#topicModelModal').modal('hide'); + localStorage.tm = topicModel.id; }; $scope.menubarSearch = function(query) { @@ -118,8 +134,8 @@ /** * Index controller */ - app.controller('IndexController', ['$scope', '$stateParams', '$location', 'ArticleFactory', 'TopicFactory', 'SearchFactory', - function($scope, $stateParams, $location, ArticleFactory, TopicFactory, SearchFactory) { + app.controller('IndexController', ['$scope', '$stateParams', '$location', '$timeout', 'ArticleFactory', 'TopicFactory', 'SearchFactory', + function($scope, $stateParams, $location, $timeout, ArticleFactory, TopicFactory, SearchFactory) { // page was reloaded, choose topic model if (!$scope.rootModels.topicModel) @@ -147,7 +163,7 @@ }); }); - $scope.$watchGroup(['search', 'rootModels.topicModel'], function() { + $scope.$watchGroup(['search', 'rootModels.topicModel', 'rootModels.advFromDate', 'rootModels.advToDate'], function() { if ($scope.search && $scope.rootModels.topicModel) { $location.search('q', $scope.search); $scope.goSearch(); @@ -163,7 +179,9 @@ SearchFactory.query({ topicModel: $scope.rootModels.topicModel.id, limit: 10, - query: $scope.search + query: $scope.search, + from: $scope.rootModels.advFromDate ? $scope.rootModels.advFromDate.getTime() : null, + to: $scope.rootModels.advToDate ? $scope.rootModels.advToDate.getTime() : null }, function(data) { $scope.searching = false; $scope.searchResults = data; diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index 68ff167810d98352abda5eb2d3db9ac0f8a8e0ae..ef7b1f8ccb78c4e8e85851c0c0506a0679eaf0b8 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -172,6 +172,29 @@ } }]); + app.directive('bsDatetimepicker', [function() { + return { + scope: { + ngModel: '=' + }, + link: function($scope, $elem) { + $elem.datetimepicker({ + sideBySide: true, + calendarWeeks: true, + showTodayButton: true, + showClear: true, + toolbarPlacement: 'top', + useCurrent: false + }); + $elem.on('dp.change', function(e) { + $scope.$apply(function() { + $scope.ngModel = e.date.toDate(); + }); + }); + } + } + }]); + app.directive('sequenceDropdown', [function() { return { scope: { diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index 7201c211439f0ac3c527a3f7dbdfe49ac9e59a67..ef6b0f4aff8217f94c2bf8dc336f4174d3cae1ef 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -428,6 +428,10 @@ word-menu { border-top: 1px solid #e5e5e5; } +.search-row { + margin-top: 35px; +} + @-moz-keyframes spin { 100% { -moz-transform: rotateY(360deg); diff --git a/vipra-ui/bower.json b/vipra-ui/bower.json index 11a5e687589b3f2c35b34590fffe563712a062fc..1f12c078c9f2b4dd1951888e967c28d1f96f7c11 100644 --- a/vipra-ui/bower.json +++ b/vipra-ui/bower.json @@ -31,6 +31,7 @@ "awesome-bootstrap-checkbox": "^0.x", "randomcolor": "randomColor#^0.x", "bootbox.js": "bootbox#^4.x", - "angular-hotkeys": "chieffancypants/angular-hotkeys#^1.x" + "angular-hotkeys": "chieffancypants/angular-hotkeys#^1.x", + "eonasdan-bootstrap-datetimepicker": "^4.17.37" } -} \ No newline at end of file +} diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js index 5ad351f6952a55416b56a8ab28c45f0834589a0a..e6be12b1da379f54370523ca23e2f93918b928d7 100644 --- a/vipra-ui/gulpfile.js +++ b/vipra-ui/gulpfile.js @@ -20,10 +20,11 @@ var assets = { 'bower_components/bootstrap/dist/js/bootstrap.min.js', 'bower_components/highcharts/highstock.js', 'bower_components/vis/dist/vis.min.js', - 'bower_components/moment/min/moment.min.js', + 'bower_components/moment/min/moment-with-locales.min.js', 'bower_components/nya-bootstrap-select/dist/js/nya-bs-select.min.js', 'bower_components/randomcolor/randomColor.js', - 'bower_components/bootbox.js/bootbox.js' + 'bower_components/bootbox.js/bootbox.js', + 'bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js' ], css: [ 'bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -31,7 +32,8 @@ var assets = { 'bower_components/vis/dist/vis.min.css', 'bower_components/nya-bootstrap-select/dist/css/nya-bs-select.min.css', 'bower_components/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css', - 'bower_components/angular-hotkeys/build/hotkeys.min.css' + 'bower_components/angular-hotkeys/build/hotkeys.min.css', + 'bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css' ], fonts: [ 'bower_components/bootstrap/dist/fonts/*', diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java index 3feb126a7662f10e97a0b0cff905e01ad2a992cc..2053f17db14191c98989a28062176152141fa7fd 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java +++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java @@ -62,7 +62,7 @@ public class ArticleFull implements Model<ObjectId>, Serializable { @QueryIgnore(multi = true) private List<TopicShare> topics; - private int topicsCount; + private Integer topicsCount; @Embedded @QueryIgnore(multi = true) @@ -72,6 +72,10 @@ public class ArticleFull implements Model<ObjectId>, Serializable { @QueryIgnore(all = true) private List<ArticleWord> words; + @Embedded + @QueryIgnore(all = true) + private List<TextEntity> entities; + @Embedded @QueryIgnore(multi = true) private ArticleStats stats; @@ -178,7 +182,7 @@ public class ArticleFull implements Model<ObjectId>, Serializable { topicsCount = topics == null ? 0 : topics.size(); } - public int getTopicsCount() { + public Integer getTopicsCount() { return topicsCount; } @@ -210,6 +214,14 @@ public class ArticleFull implements Model<ObjectId>, Serializable { this.words = words; } + public List<TextEntity> getEntities() { + return entities; + } + + public void setEntities(List<TextEntity> entities) { + this.entities = entities; + } + public ArticleStats getStats() { return stats; } diff --git a/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java b/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java index 49dfe53d03506e67024f3bcc5b75cb92a92b3f73..e66fa89f2753e395de3e6970cf0709978c912029 100644 --- a/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java +++ b/vipra-util/src/main/java/de/vipra/util/model/TextEntity.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) @SuppressWarnings("serial") @Embedded -public class TextEntity implements Serializable { +public class TextEntity implements Comparable<TextEntity>, Serializable { private String entity; @@ -38,4 +38,34 @@ public class TextEntity implements Serializable { this.url = url; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((entity == null) ? 0 : entity.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TextEntity other = (TextEntity) obj; + if (entity == null) { + if (other.entity != null) + return false; + } else if (!entity.equals(other.entity)) + return false; + return true; + } + + @Override + public int compareTo(TextEntity o) { + return entity.compareTo(o.getEntity()); + } + }