From 698ff58aa3c8652aa1577b8191c6ac500d96be4c Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Mon, 1 Mar 2021 11:50:07 +0100 Subject: [PATCH] Provide a more convenient interface to search --- .../docspell/backend/ops/OSimpleSearch.scala | 177 +++++++++++++++++- .../docspell/common/ItemQueryString.scala | 6 + .../scala/docspell/store/queries/Query.scala | 30 ++- 3 files changed, 204 insertions(+), 9 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala index 0ec0d903..d4a29a7f 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala @@ -1,13 +1,180 @@ package docspell.backend.ops -import docspell.backend.ops.OItemSearch.ListItemWithTags -import docspell.common.ItemQueryString -import docspell.store.qb.Batch +import cats.implicits._ +import docspell.common._ +import docspell.store.qb.Batch +import docspell.store.queries.Query +import docspell.query.{ItemQueryParser, ParseFailure} + +import OSimpleSearch._ +import docspell.store.queries.SearchSummary +import cats.effect.Sync + +/** A "porcelain" api on top of OFulltext and OItemSearch. */ trait OSimpleSearch[F[_]] { - def searchByString(q: ItemQueryString, batch: Batch): F[Vector[ListItemWithTags]] + def search(settings: Settings)(q: Query, fulltextQuery: Option[String]): F[Items] + def searchSummary( + settings: Settings + )(q: Query, fulltextQuery: Option[String]): F[SearchSummary] + + def searchByString( + settings: Settings + )(fix: Query.Fix, q: ItemQueryString): Either[ParseFailure, F[Items]] + def searchSummaryByString( + settings: Settings + )(fix: Query.Fix, q: ItemQueryString): Either[ParseFailure, F[SearchSummary]] } -object OSimpleSearch {} +object OSimpleSearch { + + final case class Settings( + batch: Batch, + useFTS: Boolean, + resolveDetails: Boolean, + maxNoteLen: Int + ) + object Settings { + def plain(batch: Batch, useFulltext: Boolean, maxNoteLen: Int): Settings = + Settings(batch, useFulltext, false, maxNoteLen) + def detailed(batch: Batch, useFulltext: Boolean, maxNoteLen: Int): Settings = + Settings(batch, useFulltext, true, maxNoteLen) + } + + sealed trait Items { + def fold[A]( + f1: Vector[OFulltext.FtsItem] => A, + f2: Vector[OFulltext.FtsItemWithTags] => A, + f3: Vector[OItemSearch.ListItem] => A, + f4: Vector[OItemSearch.ListItemWithTags] => A + ): A + + } + object Items { + def ftsItems(items: Vector[OFulltext.FtsItem]): Items = + FtsItems(items) + + case class FtsItems(items: Vector[OFulltext.FtsItem]) extends Items { + def fold[A]( + f1: Vector[OFulltext.FtsItem] => A, + f2: Vector[OFulltext.FtsItemWithTags] => A, + f3: Vector[OItemSearch.ListItem] => A, + f4: Vector[OItemSearch.ListItemWithTags] => A + ): A = f1(items) + + } + + def ftsItemsFull(items: Vector[OFulltext.FtsItemWithTags]): Items = + FtsItemsFull(items) + + case class FtsItemsFull(items: Vector[OFulltext.FtsItemWithTags]) extends Items { + def fold[A]( + f1: Vector[OFulltext.FtsItem] => A, + f2: Vector[OFulltext.FtsItemWithTags] => A, + f3: Vector[OItemSearch.ListItem] => A, + f4: Vector[OItemSearch.ListItemWithTags] => A + ): A = f2(items) + } + + def itemsPlain(items: Vector[OItemSearch.ListItem]): Items = + ItemsPlain(items) + + case class ItemsPlain(items: Vector[OItemSearch.ListItem]) extends Items { + def fold[A]( + f1: Vector[OFulltext.FtsItem] => A, + f2: Vector[OFulltext.FtsItemWithTags] => A, + f3: Vector[OItemSearch.ListItem] => A, + f4: Vector[OItemSearch.ListItemWithTags] => A + ): A = f3(items) + } + + def itemsFull(items: Vector[OItemSearch.ListItemWithTags]): Items = + ItemsFull(items) + + case class ItemsFull(items: Vector[OItemSearch.ListItemWithTags]) extends Items { + def fold[A]( + f1: Vector[OFulltext.FtsItem] => A, + f2: Vector[OFulltext.FtsItemWithTags] => A, + f3: Vector[OItemSearch.ListItem] => A, + f4: Vector[OItemSearch.ListItemWithTags] => A + ): A = f4(items) + } + + } + + def apply[F[_]: Sync](fts: OFulltext[F], is: OItemSearch[F]): OSimpleSearch[F] = + new Impl(fts, is) + + final class Impl[F[_]: Sync](fts: OFulltext[F], is: OItemSearch[F]) + extends OSimpleSearch[F] { + def searchByString( + settings: Settings + )(fix: Query.Fix, q: ItemQueryString): Either[ParseFailure, F[Items]] = + ItemQueryParser + .parse(q.query) + .map(iq => Query(fix, Query.QueryExpr(iq))) + .map(search(settings)(_, None)) //TODO resolve content:xyz expressions + + def searchSummaryByString( + settings: Settings + )(fix: Query.Fix, q: ItemQueryString): Either[ParseFailure, F[SearchSummary]] = + ItemQueryParser + .parse(q.query) + .map(iq => Query(fix, Query.QueryExpr(iq))) + .map(searchSummary(settings)(_, None)) //TODO resolve content:xyz expressions + + def searchSummary( + settings: Settings + )(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = + fulltextQuery match { + case Some(ftq) if settings.useFTS => + if (q.isEmpty) + fts.findIndexOnlySummary(q.fix.account, OFulltext.FtsInput(ftq)) + else + fts + .findItemsSummary(q, OFulltext.FtsInput(ftq)) + + case _ => + is.findItemsSummary(q) + } + + def search(settings: Settings)(q: Query, fulltextQuery: Option[String]): F[Items] = + // 1. fulltext only if fulltextQuery.isDefined && q.isEmpty && useFTS + // 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS + // 3. sql-only else (if fulltextQuery.isEmpty || !useFTS) + fulltextQuery match { + case Some(ftq) if settings.useFTS => + if (q.isEmpty) + fts + .findIndexOnly(settings.maxNoteLen)( + OFulltext.FtsInput(ftq), + q.fix.account, + settings.batch + ) + .map(Items.ftsItemsFull) + else if (settings.resolveDetails) + fts + .findItemsWithTags(settings.maxNoteLen)( + q, + OFulltext.FtsInput(ftq), + settings.batch + ) + .map(Items.ftsItemsFull) + else + fts + .findItems(settings.maxNoteLen)(q, OFulltext.FtsInput(ftq), settings.batch) + .map(Items.ftsItems) + + case _ => + if (settings.resolveDetails) + is.findItemsWithTags(settings.maxNoteLen)(q, settings.batch) + .map(Items.itemsFull) + else + is.findItems(settings.maxNoteLen)(q, settings.batch) + .map(Items.itemsPlain) + } + } + +} diff --git a/modules/common/src/main/scala/docspell/common/ItemQueryString.scala b/modules/common/src/main/scala/docspell/common/ItemQueryString.scala index 49bc878f..8e6e887e 100644 --- a/modules/common/src/main/scala/docspell/common/ItemQueryString.scala +++ b/modules/common/src/main/scala/docspell/common/ItemQueryString.scala @@ -1,3 +1,9 @@ package docspell.common case class ItemQueryString(query: String) + +object ItemQueryString { + + def apply(qs: Option[String]): ItemQueryString = + ItemQueryString(qs.getOrElse("")) +} diff --git a/modules/store/src/main/scala/docspell/store/queries/Query.scala b/modules/store/src/main/scala/docspell/store/queries/Query.scala index 879d15b0..6be04a1e 100644 --- a/modules/store/src/main/scala/docspell/store/queries/Query.scala +++ b/modules/store/src/main/scala/docspell/store/queries/Query.scala @@ -14,6 +14,12 @@ case class Query(fix: Query.Fix, cond: Query.QueryCond) { def withFix(f: Query.Fix => Query.Fix): Query = copy(fix = f(fix)) + + def isEmpty: Boolean = + fix.isEmpty && cond.isEmpty + + def nonEmpty: Boolean = + !isEmpty } object Query { @@ -22,9 +28,18 @@ object Query { account: AccountId, itemIds: Option[Set[Ident]], orderAsc: Option[RItem.Table => Column[_]] - ) + ) { - sealed trait QueryCond + def isEmpty: Boolean = + itemIds.isEmpty + } + + sealed trait QueryCond { + def isEmpty: Boolean + + def nonEmpty: Boolean = + !isEmpty + } case class QueryForm( name: Option[String], @@ -47,7 +62,11 @@ object Query { itemIds: Option[Set[Ident]], customValues: Seq[CustomValue], source: Option[String] - ) extends QueryCond + ) extends QueryCond { + + def isEmpty: Boolean = + this == QueryForm.empty + } object QueryForm { val empty = QueryForm( @@ -74,7 +93,10 @@ object Query { ) } - case class QueryExpr(q: ItemQuery) extends QueryCond + case class QueryExpr(q: ItemQuery) extends QueryCond { + def isEmpty: Boolean = + q.expr == ItemQuery.all.expr + } def empty(account: AccountId): Query = Query(Fix(account, None, None), QueryForm.empty)