Basic poc to search via custom query

This commit is contained in:
Eike Kettner 2021-02-25 22:40:17 +01:00
parent 186014a1c6
commit e9ed998e3a
10 changed files with 97 additions and 23 deletions

View File

@ -164,7 +164,7 @@ object OFulltext {
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
.compile
.to(Set)
q = Query.empty(account).withCond(_.copy(itemIds = itemIds.some))
q = Query.empty(account).withFix(_.copy(itemIds = itemIds.some))
res <- store.transact(QItem.searchStats(q))
} yield res
}
@ -220,7 +220,7 @@ object OFulltext {
.flatMap(r => Stream.emits(r.results.map(_.itemId)))
.compile
.to(Set)
qnext = q.withCond(_.copy(itemIds = items.some))
qnext = q.withFix(_.copy(itemIds = items.some))
res <- store.transact(QItem.searchStats(qnext))
} yield res

View File

@ -0,0 +1,13 @@
package docspell.backend.ops
import docspell.backend.ops.OItemSearch.ListItemWithTags
import docspell.common.ItemQueryString
import docspell.store.qb.Batch
trait OSimpleSearch[F[_]] {
def searchByString(q: ItemQueryString, batch: Batch): F[Vector[ListItemWithTags]]
}
object OSimpleSearch {}

View File

@ -0,0 +1,3 @@
package docspell.common
case class ItemQueryString(query: String)

View File

@ -73,8 +73,8 @@ object NotifyDueItemsTask {
Query
.empty(ctx.args.account)
.withOrder(orderAsc = _.dueDate)
.withCond(
_.copy(
.withCond(_ =>
Query.QueryForm.empty.copy(
states = ItemState.validStates.toList,
tagsInclude = ctx.args.tagsInclude,
tagsExclude = ctx.args.tagsExclude,

View File

@ -12,7 +12,7 @@ object ItemQueryParser {
ExprParser.exprParser
.parseAll(input.trim)
.left
.map(_.toString)
.map(pe => s"Error parsing: '${input.trim}': $pe")
.map(expr => ItemQuery(expr, Some(input.trim)))
def parseUnsafe(input: String): ItemQuery =

View File

@ -145,8 +145,8 @@ trait Conversions {
def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query =
OItemSearch.Query(
OItemSearch.Query.Fix(account, None),
OItemSearch.Query.QueryCond(
OItemSearch.Query.Fix(account, None, None),
OItemSearch.Query.QueryForm(
m.name,
if (m.inbox) Seq(ItemState.Created)
else ItemState.validStates.toList,

View File

@ -25,5 +25,9 @@ object QueryParam {
object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q")
object Query extends OptionalQueryParamDecoderMatcher[String]("q")
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
}

View File

@ -4,21 +4,20 @@ import cats.Monoid
import cats.data.NonEmptyList
import cats.effect._
import cats.implicits._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
import docspell.backend.ops.OFulltext
import docspell.backend.ops.OItemSearch.Batch
import docspell.backend.ops.OItemSearch.{Batch, Query}
import docspell.common._
import docspell.common.syntax.all._
import docspell.query.ItemQueryParser
import docspell.restapi.model._
import docspell.restserver.Config
import docspell.restserver.conv.Conversions
import docspell.restserver.http4s.BinaryUtil
import docspell.restserver.http4s.Responses
import docspell.restserver.http4s.{QueryParam => QP}
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._
@ -46,6 +45,33 @@ object ItemRoutes {
resp <- Ok(Conversions.basicResult(res, "Task submitted"))
} yield resp
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
offset
) =>
val query =
q.map(ItemQueryParser.parse) match {
case Some(Right(q)) =>
Right(Query(Query.Fix(user.account, None, None), Query.QueryExpr(q)))
case Some(Left(err)) =>
Left(err)
case None =>
Right(Query(Query.Fix(user.account, None, None), Query.QueryForm.empty))
}
val li = limit.getOrElse(cfg.maxItemPageSize)
val of = offset.getOrElse(0)
query match {
case Left(err) =>
BadRequest(BasicResult(false, err))
case Right(sq) =>
for {
items <- backend.itemSearch.findItems(cfg.maxNoteLength)(
sq,
Batch(of, li).restrictLimitTo(cfg.maxItemPageSize)
)
ok <- Ok(Conversions.mkItemList(items))
} yield ok
}
case req @ POST -> Root / "search" =>
for {
mask <- req.as[ItemSearch]

View File

@ -5,14 +5,14 @@ import cats.effect.Sync
import cats.effect.concurrent.Ref
import cats.implicits._
import fs2.Stream
import docspell.common.syntax.all._
import docspell.common.{IdRef, _}
import docspell.query.ItemQuery
import docspell.store.Store
import docspell.store.qb.DSL._
import docspell.store.qb._
import docspell.store.qb.generator.{ItemQueryGenerator, Tables}
import docspell.store.records._
import doobie.implicits._
import doobie.{Query => _, _}
import org.log4s.getLogger
@ -172,10 +172,13 @@ object QItem {
.leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll)
.leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll),
where(
i.cid === coll && or(
i.folder.isNull,
i.folder.in(QFolder.findMemberFolderIds(q.account))
i.cid === coll &&? q.itemIds.map(s =>
Nel.fromList(s.toList).map(nel => i.id.in(nel)).getOrElse(i.id.isNull)
)
&& or(
i.folder.isNull,
i.folder.in(QFolder.findMemberFolderIds(q.account))
)
)
).distinct.orderBy(
q.orderAsc
@ -184,7 +187,7 @@ object QItem {
)
}
def queryCondition(coll: Ident, q: Query.QueryCond): Condition =
def queryCondFromForm(coll: Ident, q: Query.QueryForm): Condition =
Condition.unit &&?
q.direction.map(d => i.incoming === d) &&?
q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&?
@ -221,6 +224,19 @@ object QItem {
findCustomFieldValuesForColl(coll, q.customValues)
.map(itemIds => i.id.in(itemIds))
def queryCondFromExpr(coll: Ident, q: ItemQuery): Condition = {
val tables = Tables(i, org, pers0, pers1, equip, f, a, m)
ItemQueryGenerator.fromExpr(tables, coll)(q.expr)
}
def queryCondition(coll: Ident, cond: Query.QueryCond): Condition =
cond match {
case fm: Query.QueryForm =>
queryCondFromForm(coll, fm)
case expr: Query.QueryExpr =>
queryCondFromExpr(coll, expr.q)
}
def findItems(
q: Query,
maxNoteLen: Int,

View File

@ -1,6 +1,7 @@
package docspell.store.queries
import docspell.common._
import docspell.query.ItemQuery
import docspell.store.qb.Column
import docspell.store.records.RItem
@ -9,14 +10,23 @@ case class Query(fix: Query.Fix, cond: Query.QueryCond) {
copy(cond = f(cond))
def withOrder(orderAsc: RItem.Table => Column[_]): Query =
copy(fix = fix.copy(orderAsc = Some(orderAsc)))
withFix(_.copy(orderAsc = Some(orderAsc)))
def withFix(f: Query.Fix => Query.Fix): Query =
copy(fix = f(fix))
}
object Query {
case class Fix(account: AccountId, orderAsc: Option[RItem.Table => Column[_]])
case class Fix(
account: AccountId,
itemIds: Option[Set[Ident]],
orderAsc: Option[RItem.Table => Column[_]]
)
case class QueryCond(
sealed trait QueryCond
case class QueryForm(
name: Option[String],
states: Seq[ItemState],
direction: Option[Direction],
@ -37,10 +47,10 @@ object Query {
itemIds: Option[Set[Ident]],
customValues: Seq[CustomValue],
source: Option[String]
)
object QueryCond {
) extends QueryCond
object QueryForm {
val empty =
QueryCond(
QueryForm(
None,
Seq.empty,
None,
@ -64,7 +74,9 @@ object Query {
)
}
case class QueryExpr(q: ItemQuery) extends QueryCond
def empty(account: AccountId): Query =
Query(Fix(account, None), QueryCond.empty)
Query(Fix(account, None, None), QueryForm.empty)
}