Refactor search to separate between a base query and user query

The `findBase` is adding only strictly required conditions. Everything
else comes from the user.
This commit is contained in:
Eike Kettner 2021-02-24 21:57:41 +01:00
parent c3cdec416c
commit 186014a1c6
7 changed files with 129 additions and 112 deletions

View File

@ -164,7 +164,7 @@ object OFulltext {
.flatMap(r => Stream.emits(r.results.map(_.itemId))) .flatMap(r => Stream.emits(r.results.map(_.itemId)))
.compile .compile
.to(Set) .to(Set)
q = Query.empty(account).copy(itemIds = itemIds.some) q = Query.empty(account).withCond(_.copy(itemIds = itemIds.some))
res <- store.transact(QItem.searchStats(q)) res <- store.transact(QItem.searchStats(q))
} yield res } yield res
} }
@ -208,7 +208,7 @@ object OFulltext {
search <- itemSearch.findItems(0)(q, Batch.all) search <- itemSearch.findItems(0)(q, Batch.all)
fq = FtsQuery( fq = FtsQuery(
ftsQ.query, ftsQ.query,
q.account.collective, q.fix.account.collective,
search.map(_.id).toSet, search.map(_.id).toSet,
Set.empty, Set.empty,
500, 500,
@ -220,7 +220,7 @@ object OFulltext {
.flatMap(r => Stream.emits(r.results.map(_.itemId))) .flatMap(r => Stream.emits(r.results.map(_.itemId)))
.compile .compile
.to(Set) .to(Set)
qnext = q.copy(itemIds = items.some) qnext = q.withCond(_.copy(itemIds = items.some))
res <- store.transact(QItem.searchStats(qnext)) res <- store.transact(QItem.searchStats(qnext))
} yield res } yield res
@ -253,7 +253,7 @@ object OFulltext {
val sqlResult = search(q, batch) val sqlResult = search(q, batch)
val fq = FtsQuery( val fq = FtsQuery(
ftsQ.query, ftsQ.query,
q.account.collective, q.fix.account.collective,
Set.empty, Set.empty,
Set.empty, Set.empty,
0, 0,

View File

@ -138,7 +138,9 @@ object OItemSearch {
val search = QItem.findItems(q, maxNoteLen: Int, batch) val search = QItem.findItems(q, maxNoteLen: Int, batch)
store store
.transact( .transact(
QItem.findItemsWithTags(q.account.collective, search).take(batch.limit.toLong) QItem
.findItemsWithTags(q.fix.account.collective, search)
.take(batch.limit.toLong)
) )
.compile .compile
.toVector .toVector

View File

@ -72,13 +72,16 @@ object NotifyDueItemsTask {
q = q =
Query Query
.empty(ctx.args.account) .empty(ctx.args.account)
.copy( .withOrder(orderAsc = _.dueDate)
.withCond(
_.copy(
states = ItemState.validStates.toList, states = ItemState.validStates.toList,
tagsInclude = ctx.args.tagsInclude, tagsInclude = ctx.args.tagsInclude,
tagsExclude = ctx.args.tagsExclude, tagsExclude = ctx.args.tagsExclude,
dueDateFrom = ctx.args.daysBack.map(back => now - Duration.days(back.toLong)), dueDateFrom =
dueDateTo = Some(now + Duration.days(ctx.args.remindDays.toLong)), ctx.args.daysBack.map(back => now - Duration.days(back.toLong)),
orderAsc = Some(_.dueDate) dueDateTo = Some(now + Duration.days(ctx.args.remindDays.toLong))
)
) )
res <- res <-
ctx.store ctx.store

View File

@ -145,7 +145,8 @@ trait Conversions {
def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query = def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query =
OItemSearch.Query( OItemSearch.Query(
account, OItemSearch.Query.Fix(account, None),
OItemSearch.Query.QueryCond(
m.name, m.name,
if (m.inbox) Seq(ItemState.Created) if (m.inbox) Seq(ItemState.Created)
else ItemState.validStates.toList, else ItemState.validStates.toList,
@ -168,8 +169,8 @@ trait Conversions {
.map(_.ids.flatMap(i => Ident.fromString(i).toOption).toSet) .map(_.ids.flatMap(i => Ident.fromString(i).toOption).toSet)
.filter(_.nonEmpty), .filter(_.nonEmpty),
m.customValues.map(mkCustomValue), m.customValues.map(mkCustomValue),
m.source, m.source
None )
) )
def mkCustomValue(v: CustomFieldValue): OItemSearch.CustomValue = def mkCustomValue(v: CustomFieldValue): OItemSearch.CustomValue =
@ -182,7 +183,7 @@ trait Conversions {
ItemLightGroup(g._1, g._2.map(mkItemLight).toList) ItemLightGroup(g._1, g._2.map(mkItemLight).toList)
val gs = val gs =
groups.map(mkGroup _).toList.sortWith((g1, g2) => g1.name.compareTo(g2.name) >= 0) groups.map(mkGroup).toList.sortWith((g1, g2) => g1.name.compareTo(g2.name) >= 0)
ItemLightList(gs) ItemLightList(gs)
} }

View File

@ -117,7 +117,7 @@ object QItem {
.map(nel => intersect(nel.map(singleSelect))) .map(nel => intersect(nel.map(singleSelect)))
} }
private def findItemsBase(q: Query, noteMaxLen: Int): Select = { private def findItemsBase(q: Query.Fix, noteMaxLen: Int): Select = {
object Attachs extends TableDef { object Attachs extends TableDef {
val tableName = "attachs" val tableName = "attachs"
val aliasName = "cta" val aliasName = "cta"
@ -128,7 +128,7 @@ object QItem {
val coll = q.account.collective val coll = q.account.collective
val baseSelect = Select( Select(
select( select(
i.id.s, i.id.s,
i.name.s, i.name.s,
@ -172,27 +172,23 @@ object QItem {
.leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll) .leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll)
.leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll), .leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll),
where( where(
i.cid === coll &&? Nel.fromList(q.states.toList).map(nel => i.state.in(nel)) && i.cid === coll && or(
or(i.folder.isNull, i.folder.in(QFolder.findMemberFolderIds(q.account))) i.folder.isNull,
i.folder.in(QFolder.findMemberFolderIds(q.account))
)
) )
).distinct.orderBy( ).distinct.orderBy(
q.orderAsc q.orderAsc
.map(of => OrderBy.asc(coalesce(of(i).s, i.created.s).s)) .map(of => OrderBy.asc(coalesce(of(i).s, i.created.s).s))
.getOrElse(OrderBy.desc(coalesce(i.itemDate.s, i.created.s).s)) .getOrElse(OrderBy.desc(coalesce(i.itemDate.s, i.created.s).s))
) )
findCustomFieldValuesForColl(coll, q.customValues) match {
case Some(itemIds) =>
baseSelect.changeWhere(c => c && i.id.in(itemIds))
case None =>
baseSelect
}
} }
def queryCondition(q: Query): Condition = def queryCondition(coll: Ident, q: Query.QueryCond): Condition =
Condition.unit &&? Condition.unit &&?
q.direction.map(d => i.incoming === d) &&? q.direction.map(d => i.incoming === d) &&?
q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&? q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&?
Nel.fromList(q.states.toList).map(nel => i.state.in(nel)) &&?
q.allNames q.allNames
.map(QueryWildcard.lower) .map(QueryWildcard.lower)
.map(n => .map(n =>
@ -221,15 +217,17 @@ object QItem {
.map(subsel => i.id.in(subsel)) &&? .map(subsel => i.id.in(subsel)) &&?
TagItemName TagItemName
.itemsWithEitherTagOrCategory(q.tagsExclude, q.tagCategoryExcl) .itemsWithEitherTagOrCategory(q.tagsExclude, q.tagCategoryExcl)
.map(subsel => i.id.notIn(subsel)) .map(subsel => i.id.notIn(subsel)) &&?
findCustomFieldValuesForColl(coll, q.customValues)
.map(itemIds => i.id.in(itemIds))
def findItems( def findItems(
q: Query, q: Query,
maxNoteLen: Int, maxNoteLen: Int,
batch: Batch batch: Batch
): Stream[ConnectionIO, ListItem] = { ): Stream[ConnectionIO, ListItem] = {
val sql = findItemsBase(q, maxNoteLen) val sql = findItemsBase(q.fix, maxNoteLen)
.changeWhere(c => c && queryCondition(q)) .changeWhere(c => c && queryCondition(q.fix.account.collective, q.cond))
.limit(batch) .limit(batch)
.build .build
logger.trace(s"List $batch items: $sql") logger.trace(s"List $batch items: $sql")
@ -251,10 +249,10 @@ object QItem {
.innerJoin(i, i.id === ti.itemId) .innerJoin(i, i.id === ti.itemId)
val tagCloud = val tagCloud =
findItemsBase(q, 0).unwrap findItemsBase(q.fix, 0).unwrap
.withSelect(select(tag.all).append(count(i.id).as("num"))) .withSelect(select(tag.all).append(count(i.id).as("num")))
.changeFrom(_.prepend(tagFrom)) .changeFrom(_.prepend(tagFrom))
.changeWhere(c => c && queryCondition(q)) .changeWhere(c => c && queryCondition(q.fix.account.collective, q.cond))
.groupBy(tag.tid) .groupBy(tag.tid)
.build .build
.query[TagCount] .query[TagCount]
@ -264,24 +262,24 @@ object QItem {
// are not included they are fetched separately // are not included they are fetched separately
for { for {
existing <- tagCloud existing <- tagCloud
other <- RTag.findOthers(q.account.collective, existing.map(_.tag.tagId)) other <- RTag.findOthers(q.fix.account.collective, existing.map(_.tag.tagId))
} yield existing ++ other.map(TagCount(_, 0)) } yield existing ++ other.map(TagCount(_, 0))
} }
def searchCountSummary(q: Query): ConnectionIO[Int] = def searchCountSummary(q: Query): ConnectionIO[Int] =
findItemsBase(q, 0).unwrap findItemsBase(q.fix, 0).unwrap
.withSelect(Nel.of(count(i.id).as("num"))) .withSelect(Nel.of(count(i.id).as("num")))
.changeWhere(c => c && queryCondition(q)) .changeWhere(c => c && queryCondition(q.fix.account.collective, q.cond))
.build .build
.query[Int] .query[Int]
.unique .unique
def searchFolderSummary(q: Query): ConnectionIO[List[FolderCount]] = { def searchFolderSummary(q: Query): ConnectionIO[List[FolderCount]] = {
val fu = RUser.as("fu") val fu = RUser.as("fu")
findItemsBase(q, 0).unwrap findItemsBase(q.fix, 0).unwrap
.withSelect(select(f.id, f.name, f.owner, fu.login).append(count(i.id).as("num"))) .withSelect(select(f.id, f.name, f.owner, fu.login).append(count(i.id).as("num")))
.changeFrom(_.innerJoin(fu, fu.uid === f.owner)) .changeFrom(_.innerJoin(fu, fu.uid === f.owner))
.changeWhere(c => c && queryCondition(q)) .changeWhere(c => c && queryCondition(q.fix.account.collective, q.cond))
.groupBy(f.id, f.name, f.owner, fu.login) .groupBy(f.id, f.name, f.owner, fu.login)
.build .build
.query[FolderCount] .query[FolderCount]
@ -295,9 +293,9 @@ object QItem {
.innerJoin(i, i.id === cv.itemId) .innerJoin(i, i.id === cv.itemId)
val base = val base =
findItemsBase(q, 0).unwrap findItemsBase(q.fix, 0).unwrap
.changeFrom(_.prepend(fieldJoin)) .changeFrom(_.prepend(fieldJoin))
.changeWhere(c => c && queryCondition(q)) .changeWhere(c => c && queryCondition(q.fix.account.collective, q.cond))
.groupBy(GroupBy(cf.all)) .groupBy(GroupBy(cf.all))
val basicFields = Nel.of( val basicFields = Nel.of(
@ -374,7 +372,7 @@ object QItem {
) )
) )
val from = findItemsBase(q, maxNoteLen) val from = findItemsBase(q.fix, maxNoteLen)
.appendCte(cte) .appendCte(cte)
.appendSelect(Tids.weight.s) .appendSelect(Tids.weight.s)
.changeFrom(_.innerJoin(Tids, Tids.itemId === i.id)) .changeFrom(_.innerJoin(Tids, Tids.itemId === i.id))

View File

@ -1,10 +1,22 @@
package docspell.store.queries package docspell.store.queries
import docspell.common._ import docspell.common._
import docspell.store.qb.Column
import docspell.store.records.RItem import docspell.store.records.RItem
case class Query( case class Query(fix: Query.Fix, cond: Query.QueryCond) {
account: AccountId, def withCond(f: Query.QueryCond => Query.QueryCond): Query =
copy(cond = f(cond))
def withOrder(orderAsc: RItem.Table => Column[_]): Query =
copy(fix = fix.copy(orderAsc = Some(orderAsc)))
}
object Query {
case class Fix(account: AccountId, orderAsc: Option[RItem.Table => Column[_]])
case class QueryCond(
name: Option[String], name: Option[String],
states: Seq[ItemState], states: Seq[ItemState],
direction: Option[Direction], direction: Option[Direction],
@ -24,14 +36,11 @@ case class Query(
allNames: Option[String], allNames: Option[String],
itemIds: Option[Set[Ident]], itemIds: Option[Set[Ident]],
customValues: Seq[CustomValue], customValues: Seq[CustomValue],
source: Option[String], source: Option[String]
orderAsc: Option[RItem.Table => docspell.store.qb.Column[_]]
) )
object QueryCond {
object Query { val empty =
def empty(account: AccountId): Query = QueryCond(
Query(
account,
None, None,
Seq.empty, Seq.empty,
None, None,
@ -51,7 +60,11 @@ object Query {
None, None,
None, None,
Seq.empty, Seq.empty,
None,
None None
) )
} }
def empty(account: AccountId): Query =
Query(Fix(account, None), QueryCond.empty)
}

View File

@ -23,7 +23,7 @@ object ItemQueryGeneratorTest extends SimpleTestSuite {
RAttachmentMeta.as("m") RAttachmentMeta.as("m")
) )
test("migration") { test("basic test") {
val q = ItemQueryParser val q = ItemQueryParser
.parseUnsafe("(& name:hello date>=2020-02-01 (| source=expense folder=test ))") .parseUnsafe("(& name:hello date>=2020-02-01 (| source=expense folder=test ))")
val cond = ItemQueryGenerator(tables, Ident.unsafe("coll"))(q) val cond = ItemQueryGenerator(tables, Ident.unsafe("coll"))(q)