Correctly count tag categories

If multiple tags of the same category are applied to the same item,
just summing tag counts will produce the wrong results as now items
are counted multiple times.
This commit is contained in:
Eike Kettner
2021-04-11 13:57:49 +02:00
parent 132730b19c
commit 3e0914ece7
10 changed files with 79 additions and 8 deletions

View File

@ -71,6 +71,9 @@ object OCollective {
type TagCount = docspell.store.queries.TagCount type TagCount = docspell.store.queries.TagCount
val TagCount = docspell.store.queries.TagCount val TagCount = docspell.store.queries.TagCount
type CategoryCount = docspell.store.queries.CategoryCount
val CategoryCount = docspell.store.queries.CategoryCount
type InsightData = QCollective.InsightData type InsightData = QCollective.InsightData
val insightData = QCollective.InsightData val insightData = QCollective.InsightData

View File

@ -4242,6 +4242,7 @@ components:
required: required:
- count - count
- tagCloud - tagCloud
- tagCategoryCloud
- fieldStats - fieldStats
- folderStats - folderStats
properties: properties:
@ -4250,6 +4251,8 @@ components:
format: int32 format: int32
tagCloud: tagCloud:
$ref: "#/components/schemas/TagCloud" $ref: "#/components/schemas/TagCloud"
tagCategoryCloud:
$ref: "#/components/schemas/NameCloud"
fieldStats: fieldStats:
type: array type: array
items: items:
@ -4354,7 +4357,7 @@ components:
$ref: "#/components/schemas/TagCount" $ref: "#/components/schemas/TagCount"
TagCount: TagCount:
description: | description: |
Generic structure for counting something. Structure for counting tags.
required: required:
- tag - tag
- count - count
@ -4364,6 +4367,30 @@ components:
count: count:
type: integer type: integer
format: int32 format: int32
NameCloud:
description: |
A set of counters.
required:
- items
properties:
items:
type: array
items:
$ref: "#/components/schemas/NameCount"
NameCount:
description: |
Generic structure for counting something.
required:
- name
- count
properties:
name:
type: string
count:
type: integer
format: int32
AttachmentMeta: AttachmentMeta:
description: | description: |
Extracted meta data of an attachment. Extracted meta data of an attachment.

View File

@ -31,6 +31,7 @@ trait Conversions {
SearchStats( SearchStats(
sum.count, sum.count,
mkTagCloud(sum.tags), mkTagCloud(sum.tags),
mkTagCategoryCloud(sum.cats),
sum.fields.map(mkFieldStats), sum.fields.map(mkFieldStats),
sum.folders.map(mkFolderStats) sum.folders.map(mkFolderStats)
) )
@ -63,6 +64,9 @@ trait Conversions {
def mkTagCloud(tags: List[OCollective.TagCount]) = def mkTagCloud(tags: List[OCollective.TagCount]) =
TagCloud(tags.map(tc => TagCount(mkTag(tc.tag), tc.count))) TagCloud(tags.map(tc => TagCount(mkTag(tc.tag), tc.count)))
def mkTagCategoryCloud(tags: List[OCollective.CategoryCount]) =
NameCloud(tags.map(tc => NameCount(tc.category, tc.count)))
// attachment meta // attachment meta
def mkAttachmentMeta(rm: RAttachmentMeta): AttachmentMeta = def mkAttachmentMeta(rm: RAttachmentMeta): AttachmentMeta =
AttachmentMeta( AttachmentMeta(

View File

@ -9,11 +9,11 @@ object DBFunction {
val countAll: DBFunction = CountAll val countAll: DBFunction = CountAll
def countAs[A](column: Column[A]): DBFunction = def countAs[A](column: Column[A]): DBFunction =
Count(column) Count(column, false)
case object CountAll extends DBFunction case object CountAll extends DBFunction
case class Count(column: Column[_]) extends DBFunction case class Count(column: Column[_], distinct: Boolean) extends DBFunction
case class Max(expr: SelectExpr) extends DBFunction case class Max(expr: SelectExpr) extends DBFunction

View File

@ -63,7 +63,10 @@ trait DSL extends DoobieMeta {
FromExpr.From(sel, alias) FromExpr.From(sel, alias)
def count(c: Column[_]): DBFunction = def count(c: Column[_]): DBFunction =
DBFunction.Count(c) DBFunction.Count(c, false)
def countDistinct(c: Column[_]): DBFunction =
DBFunction.Count(c, true)
def countAll: DBFunction = def countAll: DBFunction =
DBFunction.CountAll DBFunction.CountAll

View File

@ -13,8 +13,9 @@ object DBFunctionBuilder extends CommonBuilder {
case DBFunction.CountAll => case DBFunction.CountAll =>
sql"COUNT(*)" sql"COUNT(*)"
case DBFunction.Count(col) => case DBFunction.Count(col, distinct) =>
sql"COUNT(" ++ column(col) ++ fr")" if (distinct) sql"COUNT(DISTINCT " ++ column(col) ++ fr")"
else sql"COUNT(" ++ column(col) ++ fr")"
case DBFunction.Max(expr) => case DBFunction.Max(expr) =>
sql"MAX(" ++ SelectExprBuilder.build(expr) ++ fr")" sql"MAX(" ++ SelectExprBuilder.build(expr) ++ fr")"

View File

@ -0,0 +1,3 @@
package docspell.store.queries
final case class CategoryCount(category: String, count: Int)

View File

@ -190,9 +190,38 @@ object QItem {
for { for {
count <- searchCountSummary(today)(q) count <- searchCountSummary(today)(q)
tags <- searchTagSummary(today)(q) tags <- searchTagSummary(today)(q)
cats <- searchTagCategorySummary(today)(q)
fields <- searchFieldSummary(today)(q) fields <- searchFieldSummary(today)(q)
folders <- searchFolderSummary(today)(q) folders <- searchFolderSummary(today)(q)
} yield SearchSummary(count, tags, fields, folders) } yield SearchSummary(count, tags, cats, fields, folders)
def searchTagCategorySummary(
today: LocalDate
)(q: Query): ConnectionIO[List[CategoryCount]] = {
val tagFrom =
from(ti)
.innerJoin(tag, tag.tid === ti.tagId)
.innerJoin(i, i.id === ti.itemId)
val tagCloud =
findItemsBase(q.fix, today, 0).unwrap
.withSelect(select(tag.category).append(countDistinct(i.id).as("num")))
.changeFrom(_.prepend(tagFrom))
.changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond))
.groupBy(tag.category)
.build
.query[CategoryCount]
.to[List]
// the previous query starts from tags, so items with tag-count=0
// are not included they are fetched separately
for {
existing <- tagCloud
allCats <- RTag.listCategories(q.fix.account.collective)
other = allCats.diff(existing.map(_.category))
} yield existing ++ other.map(CategoryCount(_, 0))
}
def searchTagSummary(today: LocalDate)(q: Query): ConnectionIO[List[TagCount]] = { def searchTagSummary(today: LocalDate)(q: Query): ConnectionIO[List[TagCount]] = {
val tagFrom = val tagFrom =

View File

@ -3,6 +3,7 @@ package docspell.store.queries
case class SearchSummary( case class SearchSummary(
count: Int, count: Int,
tags: List[TagCount], tags: List[TagCount],
cats: List[CategoryCount],
fields: List[FieldStats], fields: List[FieldStats],
folders: List[FolderCount] folders: List[FolderCount]
) )

View File

@ -2,4 +2,4 @@ package docspell.store.queries
import docspell.store.records.RTag import docspell.store.records.RTag
case class TagCount(tag: RTag, count: Int) final case class TagCount(tag: RTag, count: Int)