Resolve fulltext search queries the same way as before

For now, fulltext search is only possible when being the only term or
inside the root AND expression.
This commit is contained in:
Eike Kettner
2021-03-07 09:38:39 +01:00
parent 1c834cbb77
commit 63d146c2de
5 changed files with 257 additions and 60 deletions

View File

@ -0,0 +1,71 @@
package docspell.query
import cats._
import cats.implicits._
import docspell.query.ItemQuery.Expr.AndExpr
import docspell.query.ItemQuery.Expr.NotExpr
import docspell.query.ItemQuery.Expr.OrExpr
import docspell.query.ItemQuery._
/** Currently, fulltext in a query is only supported when in "root
* AND" position
*/
object FulltextExtract {
sealed trait Result
sealed trait SuccessResult extends Result
sealed trait FailureResult extends Result
object Result {
case class Success(query: Expr, fts: Option[String]) extends SuccessResult
case object TooMany extends FailureResult
case object UnsupportedPosition extends FailureResult
}
def findFulltext(expr: Expr): Result =
lookForFulltext(expr)
private def lookForFulltext(expr: Expr): Result =
expr match {
case Expr.Fulltext(ftq) =>
Result.Success(ItemQuery.all.expr, ftq.some)
case Expr.AndExpr(inner) =>
inner.collect({ case Expr.Fulltext(fq) => fq }) match {
case Nil =>
checkPosition(expr, 0)
case e :: Nil =>
val c = foldMap(isFulltextExpr)(expr)
if (c > 1) Result.TooMany
else Result.Success(expr, e.some)
case _ =>
Result.TooMany
}
case _ =>
checkPosition(expr, 0)
}
private def checkPosition(expr: Expr, max: Int): Result = {
val c = foldMap(isFulltextExpr)(expr)
if (c > max) Result.UnsupportedPosition
else Result.Success(expr, None)
}
private def foldMap[B: Monoid](f: Expr => B)(expr: Expr): B =
expr match {
case OrExpr(inner) =>
inner.map(foldMap(f)).fold
case AndExpr(inner) =>
inner.map(foldMap(f)).fold
case NotExpr(e) =>
f(e)
case _ =>
f(expr)
}
private def isFulltextExpr(expr: Expr): Int =
expr match {
case Expr.Fulltext(_) => 1
case _ => 0
}
}

View File

@ -11,7 +11,10 @@ import docspell.query.ItemQuery.Attr.{DateAttr, StringAttr}
* against a specific field of an item using some operator or a
* combination thereof.
*/
final case class ItemQuery(expr: ItemQuery.Expr, raw: Option[String])
final case class ItemQuery(expr: ItemQuery.Expr, raw: Option[String]) {
def findFulltext: FulltextExtract.Result =
FulltextExtract.findFulltext(expr)
}
object ItemQuery {
val all = ItemQuery(Expr.Exists(Attr.ItemId), Some(""))

View File

@ -0,0 +1,57 @@
package docspell.query
import cats.implicits._
import munit._
import docspell.query.FulltextExtract.Result
class FulltextExtractTest extends FunSuite {
def findFts(q: String): Result = {
val p = ItemQueryParser.parseUnsafe(q)
FulltextExtract.findFulltext(p.expr)
}
def assertFts(qstr: String, expect: Result) =
assertEquals(findFts(qstr), expect)
def assertFtsSuccess(qstr: String, expect: Option[String]) = {
val q = ItemQueryParser.parseUnsafe(qstr)
assertEquals(findFts(qstr), Result.Success(q.expr, expect))
}
test("find fulltext as root") {
assertEquals(findFts("content:what"), Result.Success(ItemQuery.all.expr, "what".some))
assertEquals(
findFts("content:\"what hello\""),
Result.Success(ItemQuery.all.expr, "what hello".some)
)
assertEquals(
findFts("content:\"what OR hello\""),
Result.Success(ItemQuery.all.expr, "what OR hello".some)
)
}
test("find no fulltext") {
assertFtsSuccess("name:test", None)
}
test("find fulltext within and") {
assertFtsSuccess("content:what name:test", "what".some)
assertFtsSuccess("$names:marc* content:what name:test", "what".some)
assertFtsSuccess(
"$names:marc* date:2021-02 content:\"what else\" name:test",
"what else".some
)
}
test("too many fulltext searches") {
assertFts("content:yes content:no", Result.TooMany)
assertFts("content:yes (| name:test content:no)", Result.TooMany)
assertFts("content:yes (| name:test (& date:2021-02 content:no))", Result.TooMany)
}
test("wrong fulltext search position") {
assertFts("name:test (| date:2021-02 content:yes)", Result.UnsupportedPosition)
assertFts("name:test (& date:2021-02 content:yes)", Result.UnsupportedPosition) //TODO
}
}