From 6a1f4318b0da85b24686267c72ae6e24f17594fe Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Fri, 19 Feb 2016 22:29:45 +0100
Subject: [PATCH] updated info page, fixed procession bugs

info page now shows more information
fixed a bug where text procession would not use lemmatized words
fixed a bug where text procession would add words twice
added constants for minimum likeliness and minimum word frequency
added query to count method
---
 .../vipra/rest/resource/ArticleResource.java  |   2 +-
 .../de/vipra/rest/resource/InfoResource.java  |  32 +++-
 .../de/vipra/rest/resource/TopicResource.java |  16 +-
 .../de/vipra/rest/resource/WordResource.java  |   2 +-
 .../java/de/vipra/cmd/lda/JGibbAnalyzer.java  |   7 +
 .../de/vipra/cmd/option/StatsCommand.java     |   6 +-
 .../java/de/vipra/cmd/option/TestCommand.java |   2 +-
 vipra-ui/app/html/about.html                  | 163 ++++++++++++++++++
 vipra-ui/app/html/articles/show.html          |   2 +-
 vipra-ui/app/html/topics/articles.html        |  23 ++-
 vipra-ui/app/html/topics/show.html            |   5 +-
 vipra-ui/app/html/words/show.html             |   2 +-
 vipra-ui/app/js/app.js                        |   2 +-
 vipra-ui/app/js/controllers.js                |  35 +++-
 .../main/java/de/vipra/util/Constants.java    |   9 +-
 .../java/de/vipra/util/model/TopicFull.java   |   2 +-
 .../de/vipra/util/service/MongoService.java   |  44 +++--
 .../java/de/vipra/util/service/Service.java   |   2 +-
 18 files changed, 306 insertions(+), 50 deletions(-)

diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
index d37c7e36..2d93b45e 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
@@ -63,7 +63,7 @@ public class ArticleResource {
 			List<ArticleFull> articles = dbArticles.getMultiple(skip, limit, sortBy, StringUtils.getFields(fields));
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbArticles.count());
+				res.addHeader("total", dbArticles.count(null));
 			else
 				res.addHeader("total", articles.size());
 
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java
index 9a9aac2a..fbcd8576 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java
@@ -16,6 +16,7 @@ import org.bson.types.ObjectId;
 import de.vipra.rest.model.ResponseWrapper;
 import de.vipra.util.BuildInfo;
 import de.vipra.util.Config;
+import de.vipra.util.Constants;
 import de.vipra.util.NestedMap;
 import de.vipra.util.StringUtils;
 import de.vipra.util.model.Article;
@@ -64,9 +65,34 @@ public class InfoResource {
 			info.put("app.builddate", buildInfo.getBuildDate());
 
 			// database info
-			info.put("db.articles", dbArticles.count());
-			info.put("db.topics", dbTopics.count());
-			info.put("db.words", dbWords.count());
+			info.put("db.articles", dbArticles.count(null));
+			info.put("db.topics", dbTopics.count(null));
+			info.put("db.words", dbWords.count(null));
+
+			// configuration
+			info.put("config.analyzer", config.analyzer);
+			info.put("config.processor", config.processor);
+			info.put("config.windowres", config.windowResolution);
+
+			// constants
+			info.put("const.importbuf", Constants.IMPORT_BUFFER_MAX);
+			info.put("const.esboosttopics", Constants.ES_BOOST_TOPICS);
+			info.put("const.esboosttitles", Constants.ES_BOOST_TITLES);
+			info.put("const.topicautoname", Constants.TOPIC_AUTO_NAMING_WORDS);
+			info.put("const.ktopics", Constants.K_TOPICS);
+			info.put("const.ktopicwords", Constants.K_TOPIC_WORDS);
+			info.put("const.likeprecision", Constants.LIKELINESS_PRECISION);
+			info.put("const.minimumlike", Constants.MINIMUM_LIKELINESS);
+			info.put("const.topicthresh", Constants.TOPIC_THRESHOLD);
+			info.put("const.docminfreq", Constants.DOCUMENT_MIN_WORD_FREQ);
+			info.put("const.docminlength", Constants.DOCUMENT_MIN_LENGTH);
+			info.put("const.charsdisallow", Constants.CHARS_DISALLOWED);
+			info.put("const.regexemail", Constants.REGEX_EMAIL);
+			info.put("const.regexurl", Constants.REGEX_URL);
+			info.put("const.regexnumber", Constants.REGEX_NUMBER);
+			info.put("const.regexchar", Constants.REGEX_SINGLECHAR);
+			info.put("const.excerptlength", Constants.EXCERPT_LENGTH);
+			info.put("const.dateformat", Constants.DATETIME_FORMAT);
 		} catch (Exception e) {
 			info.put("error", e.getMessage());
 		}
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
index 6a0d7bde..acbbf0a9 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
@@ -65,7 +65,7 @@ public class TopicResource {
 			List<TopicFull> topics = dbTopics.getMultiple(skip, limit, sortBy, StringUtils.getFields(fields));
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbTopics.count());
+				res.addHeader("total", dbTopics.count(null));
 			else
 				res.addHeader("total", topics.size());
 
@@ -112,14 +112,24 @@ public class TopicResource {
 	@Produces(MediaType.APPLICATION_JSON)
 	@Consumes(MediaType.APPLICATION_JSON)
 	@Path("{id}/articles")
-	public Response getArticles(@PathParam("id") String id, @QueryParam("fields") String fields) {
+	public Response getArticles(@PathParam("id") String id, @QueryParam("skip") Integer skip,
+			@QueryParam("limit") Integer limit, @QueryParam("sort") @DefaultValue("title") String sortBy,
+			@QueryParam("fields") String fields) {
 		ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>();
 		try {
 			Topic topic = new Topic(MongoUtils.objectId(id));
-			QueryBuilder query = QueryBuilder.builder().criteria("topics.topic", topic);
+			QueryBuilder query = QueryBuilder.builder().criteria("topics.topic", topic).skip(skip).limit(limit)
+					.sortBy(sortBy);
 			if (fields != null && !fields.isEmpty())
 				query.fields(true, StringUtils.getFields(fields));
+
 			List<ArticleFull> articles = dbArticles.getMultiple(query);
+
+			if ((skip != null && skip > 0) || (limit != null && limit > 0))
+				res.addHeader("total", dbArticles.count(QueryBuilder.builder().criteria("topics.topic", topic)));
+			else
+				res.addHeader("total", articles.size());
+
 			return res.ok(articles);
 		} catch (Exception e) {
 			e.printStackTrace();
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
index 951e398c..9cdb550a 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
@@ -57,7 +57,7 @@ public class WordResource {
 			List<Word> words = dbWords.getMultiple(skip, limit, sortBy, StringUtils.getFields(fields));
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbWords.count());
+				res.addHeader("total", dbWords.count(null));
 			else
 				res.addHeader("total", words.size());
 
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java
index 1845ac5a..8fa94370 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java
@@ -106,8 +106,15 @@ public class JGibbAnalyzer extends Analyzer {
 							String[] parts = nextLine.trim().split("\\s+");
 							try {
 								Word word = wordMap.get(parts[0]);
+								// round likeliness precision
 								double likeliness = NumberUtils.roundToPrecision(Double.parseDouble(parts[1]),
 										Constants.LIKELINESS_PRECISION);
+
+								// check if word likely enough to relate to
+								// topic
+								if (likeliness < Constants.MINIMUM_LIKELINESS)
+									continue;
+
 								TopicWord topicWord = new TopicWord(word, likeliness);
 								topicWords.add(topicWord);
 							} catch (NumberFormatException e) {
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java
index 31d7c3c5..225d13bf 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java
@@ -20,9 +20,9 @@ public class StatsCommand implements Command {
 	private MongoService<Word, String> dbWords;
 
 	private void stats() {
-		log.info("# of articles: " + dbArticles.count());
-		log.info("# of topics  : " + dbTopics.count());
-		log.info("# of words   : " + dbWords.count());
+		log.info("# of articles: " + dbArticles.count(null));
+		log.info("# of topics  : " + dbTopics.count(null));
+		log.info("# of words   : " + dbWords.count(null));
 	}
 
 	@Override
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java
index 96abc641..c57fe1ea 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java
@@ -24,7 +24,7 @@ public class TestCommand implements Command {
 		// test if database is accessible
 		log.info("testing mongodb connection...");
 		MongoService<Article, ObjectId> dbArticles = MongoService.getDatabaseService(config, Article.class);
-		dbArticles.count();
+		dbArticles.count(null);
 
 		// test if elasticsearch is accessible
 		log.info("testing elasticsearch connection...");
diff --git a/vipra-ui/app/html/about.html b/vipra-ui/app/html/about.html
index 2f363e4a..d040c3ea 100644
--- a/vipra-ui/app/html/about.html
+++ b/vipra-ui/app/html/about.html
@@ -98,4 +98,167 @@
       </tr>
     </tbody>
   </table>
+
+  <h3>Configuration</h3>
+
+  <table class="table table-bordered table-fixed">
+    <tbody>
+      <tr>
+        <th style="width:33%">Analyzer</th>
+        <td ng-bind-template="{{::info.config.analyzer}}"></td>
+      </tr>
+      <tr>
+        <th>Processor</th>
+        <td ng-bind-template="{{::info.config.processor}}"></td>
+      </tr>
+      <tr>
+        <th>Window resolution</th>
+        <td ng-bind-template="{{::info.config.windowres}}"></td>
+      </tr>
+    </tbody>
+  </table>
+
+  <h3>Constants</h3>
+
+  <table class="table table-bordered table-fixed">
+    <tbody>
+      <tr>
+        <th style="width:33%">Import buffer</th>
+        <td ng-bind-template="{{::info.const.importbuf}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The number of articles that are imported in one go.
+        </td>
+      </tr>
+      <tr>
+        <th>ES boost topics</th>
+        <td ng-bind-template="{{::info.const.esboosttopics}}"></td>
+      </tr>
+      <tr>
+        <th>ES boost titles</th>
+        <td ng-bind-template="{{::info.const.esboosttitles}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          Boost parameters to modify the importance of search fields. Default is 1, all greater values raise field importance.
+        </td>
+      </tr>
+      <tr>
+        <th>Topic auto naming words</th>
+        <td ng-bind-template="{{::info.const.topicautoname}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The number of topic words to be used to automatically generate a topic name. Words are sorted by descending topic association likeliness.
+        </td>
+      </tr>
+      <tr>
+        <th>K topics</th>
+        <td ng-bind-template="{{::info.const.ktopics}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The number of topics to be generated in the topic modeling process.
+        </td>
+      </tr>
+      <tr>
+        <th>K topic words</th>
+        <td ng-bind-template="{{::info.const.ktopicwords}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The maximum number of words that are associated to a single topic.
+        </td>
+      </tr>
+      <tr>
+        <th>Likeliness precision</th>
+        <td ng-bind-template="{{::info.const.likeprecision}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The resulting likeliness precision of topic words. 
+        </td>
+      </tr>
+      <tr>
+        <th>Minimum likeliness</th>
+        <td ng-bind-template="{{::info.const.minimumlike}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The minimum likeliness of topic words. Words with a lesser likeliness are ignored.
+        </td>
+      </tr>
+      <tr>
+        <th>Topic share threshold</th>
+        <td ng-bind-template="{{::info.const.topicthresh}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The minimum share value of a topic to be considered associated to an article. Topics with a lower share are ignored.
+        </td>
+      </tr>
+      <tr>
+        <th>Word minimum frequency</th>
+        <td ng-bind-template="{{::info.const.docminfreq}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The minimum word frequency for unique words in an article to be used in the topic modeling process. Unique words with a lower frequency are ignored.
+        </td>
+      </tr>
+      <tr>
+        <th>Document minimum word count</th>
+        <td ng-bind-template="{{::info.const.docminlength}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The minimum article word count. Articles with less words are not included in the topic modeling process.
+        </td>
+      </tr>
+      <tr>
+        <th>Regex disallowed chars</th>
+        <td ng-bind-template="{{::info.const.charsdisallow}}"></td>
+      </tr>
+      <tr>
+        <th>Regex email</th>
+        <td ng-bind-template="{{::info.const.regexemail}}"></td>
+      </tr>
+      <tr>
+        <th>Regex URL</th>
+        <td ng-bind-template="{{::info.const.regexurl}}"></td>
+      </tr>
+      <tr>
+        <th>Regex number</th>
+        <td ng-bind-template="{{::info.const.regexnumber}}"></td>
+      </tr>
+      <tr>
+        <th>Regex single characters</th>
+        <td ng-bind-template="{{::info.const.regexchar}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          Regular expressions used in the article text procession. These regular expressions are used to remove unwanted text passages. Each match is replaced by empty strings.
+        </td>
+      </tr>
+      <tr>
+        <th>Excerpt length</th>
+        <td ng-bind-template="{{::info.const.excerptlength}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The maximum excerpt length that is inserted in the ElasticSearch index for display on the search result page.
+        </td>
+      </tr>
+      <tr>
+        <th>Date time format</th>
+        <td ng-bind-template="{{::info.const.dateformat}}"></td>
+      </tr>
+      <tr class="well">
+        <td colspan="2">
+          The date and time format for backend-frontend communication. This is not the format used for frontend display, which is based on localization.
+        </td>
+      </tr>
+    </tbody>
+  </table>
 </div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/articles/show.html b/vipra-ui/app/html/articles/show.html
index 00b486cf..e2e4fff5 100644
--- a/vipra-ui/app/html/articles/show.html
+++ b/vipra-ui/app/html/articles/show.html
@@ -1,4 +1,4 @@
-<div ng-cloak>
+<div ui-view ng-cloak>
   <div class="page-header">
     <h1 ng-bind="::article.title"></h1>
 
diff --git a/vipra-ui/app/html/topics/articles.html b/vipra-ui/app/html/topics/articles.html
index 90d1e2d0..40c7f7b7 100644
--- a/vipra-ui/app/html/topics/articles.html
+++ b/vipra-ui/app/html/topics/articles.html
@@ -1,4 +1,4 @@
-<div ng-cloak>
+<div ui-view ng-cloak>
   <div class="page-header">
     <h1>
       <div ng-bind="topic.name" ng-hide="isRename"></div>
@@ -22,17 +22,24 @@
     <table class="item-actions">
       <tr>
         <td>
-          <bs-dropdown label="Actions">
-            <li><a ng-click="startRename()">Rename</a></li>
-          </bs-dropdown>
-        </td>
-        <td>
-          <a class="btn btn-default" ui-sref="network({type:'topics', id:topic.id})">Network graph</a>
+          <a class="btn btn-default" ui-sref="topics.show({id:topic.id})">Back</a>
         </td>
       </tr>
     </table>
   </div>
 
-  <h3>Info <hide-link target="#info"/></h3>
+  <h3>Articles</h3>
   
+  <div class="well">
+    Found <span ng-bind="articlesTotal"></span> articles in the database.<br>
+  </div>
+
+  <ul class="dashed">
+    <li ng-repeat="article in articles">
+      <a ui-sref="articles.show({id: article.id})" ng-bind="::article.title"></a>
+    </li>
+  </ul>
+
+  <pagination total="articlesTotal" page="page" limit="limit" change="changePage"/>
+
 </div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html
index 964d9743..0ebb0cf6 100644
--- a/vipra-ui/app/html/topics/show.html
+++ b/vipra-ui/app/html/topics/show.html
@@ -1,4 +1,4 @@
-<div ng-cloak>
+<div ui-view ng-cloak>
   <div class="page-header">
     <h1>
       <div ng-bind="topic.name" ng-hide="isRename"></div>
@@ -29,6 +29,9 @@
         <td>
           <a class="btn btn-default" ui-sref="network({type:'topics', id:topic.id})">Network graph</a>
         </td>
+        <td>
+          <a class="btn btn-default" ui-sref="topics.show.articles({id:topic.id})">Articles</a>
+        </td>
       </tr>
     </table>
   </div>
diff --git a/vipra-ui/app/html/words/show.html b/vipra-ui/app/html/words/show.html
index 445cc90a..6174e8c3 100644
--- a/vipra-ui/app/html/words/show.html
+++ b/vipra-ui/app/html/words/show.html
@@ -1,4 +1,4 @@
-<div ng-cloak>
+<div ui-view ng-cloak>
   <div class="page-header">
     <h1 ng-bind="::word.id"></h1>
   </div>
diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js
index a2d1b64e..acc34073 100644
--- a/vipra-ui/app/js/app.js
+++ b/vipra-ui/app/js/app.js
@@ -71,7 +71,7 @@
     });
 
     $stateProvider.state('topics.show.articles', {
-      url: '/articles',
+      url: '/articles?page',
       templateUrl: Vipra.const.tplBase + '/topics/articles.html',
       controller: 'TopicsArticlesController'
     });
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index 684e4336..ee2b2baf 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -307,8 +307,11 @@
       // calculate percentage share
       var topicShareSeries = [],
           topics = $scope.article.topics;
+          topicsCount = 0;
+      for(var i = 0; i < topics.length; i++)
+        topicsCount += topics[i].count;
       for(var i = 0; i < topics.length; i++) {
-        var share = Vipra.toPercent(topics[i].count / $scope.article.stats.wordCount);
+        var share = Vipra.toPercent(topics[i].count / topicsCount);
         topics[i].share = share;
         topicShareSeries.push({name: topics[i].topic.name.ellipsize(20), y: share});
       }
@@ -418,12 +421,32 @@
   /**
    * Topic Show Articles route
    */
-  app.controller('TopicArticlesController', ['$scope', '$stateParams', 'TopicFactory',
-    function($scope, $stateParams, TopicFactory) {
+  app.controller('TopicsArticlesController', ['$scope', '$stateParams', 'Store', 'TopicFactory',
+    function($scope, $stateParams, Store, TopicFactory) {
 
-    TopicFactory.articles({id: $stateParams.id}, function(data) {
-      $scope.articles = data;
-    });
+    $scope.page = Math.max($stateParams.page || 1, 1);
+    $scope.limit = Vipra.const.pageSize;
+    $scope.sort = Store('sortarticles') || 'title';
+    $scope.order = Store('orderarticles') || '';
+
+    $scope.reload = function() {
+      TopicFactory.articles({
+        id: $stateParams.id,
+        skip: ($scope.page-1)*$scope.limit,
+        limit: $scope.limit,
+        sort: $scope.order+$scope.sort
+      }, function(data, headers) {
+        $scope.articles = data;
+        $scope.articlesTotal = headers("V-Total");
+      });
+    };
+
+    $scope.changePage = function(page) {
+      $scope.page = page;
+      $scope.reload();
+    };
+
+    $scope.reload();
 
   }]);
 
diff --git a/vipra-util/src/main/java/de/vipra/util/Constants.java b/vipra-util/src/main/java/de/vipra/util/Constants.java
index 9f431ebb..32d35b3e 100644
--- a/vipra-util/src/main/java/de/vipra/util/Constants.java
+++ b/vipra-util/src/main/java/de/vipra/util/Constants.java
@@ -62,7 +62,7 @@ public class Constants {
 	 * The number of words to be used to generate a topic name. The top n words
 	 * (sorted by likeliness) are used to generate a name for unnamed topics.
 	 */
-	public static final int AUTO_TOPIC_WORDS = 4;
+	public static final int TOPIC_AUTO_NAMING_WORDS = 4;
 
 	/**
 	 * Number of topics to discover with topic modeling, if the selected topic
@@ -81,6 +81,11 @@ public class Constants {
 	 * belong to topics.
 	 */
 	public static final int LIKELINESS_PRECISION = 6;
+	
+	/**
+	 * Minimum likeliness of words. Words with lower likeliness are ignored
+	 */
+	public static final double MINIMUM_LIKELINESS = 0;
 
 	/**
 	 * Topics with a share greater or equal to this number are regarded as
@@ -93,7 +98,7 @@ public class Constants {
 	 * below this frequency in a document are filtered out before generating the
 	 * topic model.
 	 */
-	public static final int DOCUMENT_MIN_WORD_FREQ = 20;
+	public static final int DOCUMENT_MIN_WORD_FREQ = 10;
 
 	/**
 	 * Minumum number of words per document.
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
index 126abc28..6dd42ea1 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
@@ -114,7 +114,7 @@ public class TopicFull implements Model<ObjectId>, Serializable {
 	public static String getNameFromWords(List<TopicWord> words) {
 		String name = null;
 		if (words != null && words.size() > 0) {
-			int size = Math.min(Constants.AUTO_TOPIC_WORDS, words.size());
+			int size = Math.min(Constants.TOPIC_AUTO_NAMING_WORDS, words.size());
 			List<String> topWords = new ArrayList<>(size);
 			for (int i = 0; i < size; i++) {
 				topWords.add(words.get(i).getWord().getId());
diff --git a/vipra-util/src/main/java/de/vipra/util/service/MongoService.java b/vipra-util/src/main/java/de/vipra/util/service/MongoService.java
index e7bf6d59..2f4a4bc4 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/MongoService.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/MongoService.java
@@ -60,12 +60,12 @@ public class MongoService<Type extends Model<IdType>, IdType> implements Service
 		if (id == null)
 			throw new DatabaseException(new NullPointerException("id is null"));
 
-		Query<Type> q = datastore.createQuery(clazz).field("_id").equal(id);
+		Query<Type> query = datastore.createQuery(clazz).field("_id").equal(id);
 		if (fields != null && fields.length > 0)
-			q.retrievedFields(true, fields);
+			query.retrievedFields(true, fields);
 		else if (!allFields && ignoredFieldsSingleQuery.length > 0)
-			q.retrievedFields(false, ignoredFieldsSingleQuery);
-		Type t = q.get();
+			query.retrievedFields(false, ignoredFieldsSingleQuery);
+		Type t = query.get();
 		return t;
 	}
 
@@ -83,32 +83,32 @@ public class MongoService<Type extends Model<IdType>, IdType> implements Service
 
 	@Override
 	public List<Type> getMultiple(QueryBuilder builder) {
-		Query<Type> q = datastore.createQuery(clazz);
+		Query<Type> query = datastore.createQuery(clazz);
 
 		if (builder != null) {
 			if (builder.getSkip() != null && builder.getSkip() > 0)
-				q.offset(builder.getSkip());
+				query.offset(builder.getSkip());
 			if (builder.getLimit() != null && builder.getLimit() > 0)
-				q.limit(builder.getLimit());
+				query.limit(builder.getLimit());
 			if (builder.getSortBy() != null)
-				q.order(builder.getSortBy());
+				query.order(builder.getSortBy());
 			if (builder.getCriteria() != null)
 				for (Pair<String, Object> criteria : builder.getCriteria())
-					q.field(criteria.x()).equal(criteria.y());
+					query.field(criteria.x()).equal(criteria.y());
 			if (builder.getFields() != null) {
 				String[] fields = builder.getFields();
 				if (builder.isInclude()) {
-					q.retrievedFields(true, fields);
+					query.retrievedFields(true, fields);
 				} else {
-					q.retrievedFields(false, fields);
+					query.retrievedFields(false, fields);
 				}
 			} else if (ignoredFieldsMultiQuery.length > 0) {
-				q.retrievedFields(false, ignoredFieldsMultiQuery);
+				query.retrievedFields(false, ignoredFieldsMultiQuery);
 			}
 		} else if (ignoredFieldsMultiQuery.length > 0) {
-			q.retrievedFields(false, ignoredFieldsMultiQuery);
+			query.retrievedFields(false, ignoredFieldsMultiQuery);
 		}
-		List<Type> list = q.asList();
+		List<Type> list = query.asList();
 		return list;
 	}
 
@@ -209,8 +209,20 @@ public class MongoService<Type extends Model<IdType>, IdType> implements Service
 	}
 
 	@Override
-	public long count() {
-		return datastore.getCount(clazz);
+	public long count(QueryBuilder builder) {
+		if (builder == null)
+			return datastore.getCount(clazz);
+
+		Query<Type> query = datastore.createQuery(clazz);
+		if (builder.getSkip() != null && builder.getSkip() > 0)
+			query.offset(builder.getSkip());
+		if (builder.getLimit() != null && builder.getLimit() > 0)
+			query.limit(builder.getLimit());
+		if (builder.getCriteria() != null)
+			for (Pair<String, Object> criteria : builder.getCriteria())
+				query.field(criteria.x()).equal(criteria.y());
+
+		return datastore.getCount(query);
 	}
 
 	public static <Type extends Model<IdType>, IdType> MongoService<Type, IdType> getDatabaseService(Config config,
diff --git a/vipra-util/src/main/java/de/vipra/util/service/Service.java b/vipra-util/src/main/java/de/vipra/util/service/Service.java
index c1b9725e..d14eba74 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/Service.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/Service.java
@@ -145,7 +145,7 @@ public interface Service<Type extends Model<IdType>, IdType, E extends Exception
 	 * @return number of entities in the database
 	 * @throws E
 	 */
-	long count() throws E;
+	long count(QueryBuilder builder) throws E;
 
 	/**
 	 * QueryBuilder instances are used to create complex queries for use with
-- 
GitLab