more study search engine WIP
parent
a715691076
commit
f55da1c9a7
|
@ -81,7 +81,8 @@ final class Env(
|
|||
Env.explorer, // required to load the actor
|
||||
Env.fishnet, // required to schedule the cleaner
|
||||
Env.notifyModule, // required to load the actor
|
||||
Env.plan // required to load the actor
|
||||
Env.plan, // required to load the actor
|
||||
Env.studySearch // required to load the actor
|
||||
)) { lap =>
|
||||
lila.log("boot").info(s"${lap.millis}ms Preloading complete")
|
||||
}
|
||||
|
|
|
@ -25,8 +25,9 @@ withLangAnnotations = false) {
|
|||
<div class="content_box no_padding studies">
|
||||
<div class="top">
|
||||
<h1>@titleHtml</h1>
|
||||
<form action=
|
||||
<input name="q" placeholder="@trans.search()" />
|
||||
<form class="search" action="@routes.Study.search()" method="get">
|
||||
<input name="q" placeholder="@trans.search()" />
|
||||
</form>
|
||||
@orderChoice(o => url(o.key), order, if (active == "all") lila.study.Order.allButOldest else lila.study.Order.all)
|
||||
@newForm()
|
||||
</div>
|
||||
|
|
|
@ -24,10 +24,11 @@ menu = menu.some,
|
|||
moreCss = moreCss,
|
||||
moreJs = moreJs,
|
||||
withLangAnnotations = false) {
|
||||
<div class="content_box no_padding studies">
|
||||
<div class="content_box no_padding studies search">
|
||||
<div class="top">
|
||||
<form action="@routes.Study.search()" method="get">
|
||||
<input name="q" placeholder="@trans.search()" />
|
||||
<h1>Search studies</h1>
|
||||
<form class="search" action="@routes.Study.search()" method="get">
|
||||
<input name="q" placeholder="@trans.search()" value="@text" />
|
||||
</form>
|
||||
@newForm()
|
||||
</div>
|
||||
|
|
|
@ -537,7 +537,7 @@ study {
|
|||
studySearch {
|
||||
index = study
|
||||
paginator.max_per_page = ${study.paginator.max_per_page}
|
||||
actor.name = team-search
|
||||
actor.name = study-search
|
||||
}
|
||||
site {
|
||||
socket {
|
||||
|
@ -632,9 +632,8 @@ hub {
|
|||
actor = ${forum.actor.name}
|
||||
search = ${forumSearch.actor.name}
|
||||
}
|
||||
team {
|
||||
search = ${teamSearch.actor.name}
|
||||
}
|
||||
team.search = ${teamSearch.actor.name}
|
||||
study.search = ${studySearch.actor.name}
|
||||
fishnet = ${fishnet.actor.name}
|
||||
tournament.api = ${tournament.api_actor.name}
|
||||
timeline {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package lila.common
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object Future {
|
||||
|
||||
def lazyFold[T, R](futures: Stream[Fu[T]])(zero: R)(op: (R, T) => R): Fu[R] =
|
||||
|
@ -42,4 +44,7 @@ object Future {
|
|||
case false => find(t)(f)
|
||||
}
|
||||
}
|
||||
|
||||
def delay[A](duration: FiniteDuration)(run: => Fu[A])(implicit system: akka.actor.ActorSystem): Fu[A] =
|
||||
akka.pattern.after(duration, system.scheduler)(run)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ final class Env(config: Config, system: ActorSystem) {
|
|||
val forum = select("actor.forum.actor")
|
||||
val forumSearch = select("actor.forum.search")
|
||||
val teamSearch = select("actor.team.search")
|
||||
val studySearch = select("actor.study.search")
|
||||
val fishnet = select("actor.fishnet")
|
||||
val tournamentApi = select("actor.tournament.api")
|
||||
val simul = select("actor.simul")
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package lila.hub
|
||||
|
||||
import akka.actor._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
final class MultiThrottler(
|
||||
executionTimeout: Option[FiniteDuration] = None,
|
||||
logger: lila.log.Logger) extends Actor {
|
||||
|
||||
var executions = Map.empty[String, Unit]
|
||||
|
||||
def receive: Receive = {
|
||||
|
||||
case MultiThrottler.Work(id, run, delayOption, timeoutOption) if !executions.contains(id) =>
|
||||
implicit val system = context.system
|
||||
val fut = lila.common.Future.delay(delayOption | 0.seconds) {
|
||||
timeoutOption.orElse(executionTimeout).fold(run()) { timeout =>
|
||||
run().withTimeout(
|
||||
duration = timeout,
|
||||
error = lila.common.LilaException(s"Throttler timed out after $timeout")
|
||||
)(context.system)
|
||||
} andThenAnyway {
|
||||
self ! MultiThrottler.Done(id)
|
||||
}
|
||||
}
|
||||
executions = executions + (id -> fut)
|
||||
|
||||
case _: MultiThrottler.Work => // already executing similar work
|
||||
|
||||
case MultiThrottler.Done(id) =>
|
||||
executions = executions - id
|
||||
|
||||
case x => logger.branch("MultiThrottler").warn(s"Unsupported message $x")
|
||||
}
|
||||
}
|
||||
|
||||
object MultiThrottler {
|
||||
|
||||
case class Work(
|
||||
id: String,
|
||||
run: () => Funit,
|
||||
delay: Option[FiniteDuration],
|
||||
timeout: Option[FiniteDuration])
|
||||
|
||||
case class Done(id: String)
|
||||
|
||||
def work(
|
||||
id: String,
|
||||
run: => Funit,
|
||||
delay: Option[FiniteDuration] = None,
|
||||
timeout: Option[FiniteDuration] = None) =
|
||||
Work(id, () => run, delay, timeout)
|
||||
}
|
|
@ -85,6 +85,7 @@ final class Env(
|
|||
lightUser = getLightUser,
|
||||
scheduler = system.scheduler,
|
||||
chat = hub.actor.chat,
|
||||
indexer = hub.actor.studySearch,
|
||||
timeline = hub.actor.timeline,
|
||||
socketHub = socketHub)
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ object Node {
|
|||
|
||||
val MAX_PLIES = 400
|
||||
|
||||
case class Children(nodes: Vector[Node]) {
|
||||
case class Children(nodes: Vector[Node]) extends AnyVal {
|
||||
|
||||
def first = nodes.headOption
|
||||
def variations = nodes drop 1
|
||||
|
|
|
@ -23,6 +23,7 @@ final class StudyApi(
|
|||
lightUser: lila.common.LightUser.Getter,
|
||||
scheduler: akka.actor.Scheduler,
|
||||
chat: ActorSelection,
|
||||
indexer: ActorSelection,
|
||||
timeline: ActorSelection,
|
||||
socketHub: ActorRef) {
|
||||
|
||||
|
@ -48,6 +49,7 @@ final class StudyApi(
|
|||
studyMaker(data, user) flatMap { res =>
|
||||
studyRepo.insert(res.study) >>
|
||||
chapterRepo.insert(res.chapter) >>-
|
||||
indexStudy(res.study) >>-
|
||||
scheduleTimeline(res.study.id) inject res
|
||||
}
|
||||
|
||||
|
@ -149,14 +151,14 @@ final class StudyApi(
|
|||
studyRepo.addMember(study, StudyMember make user) >>-
|
||||
notifier(study, user, socket)
|
||||
}
|
||||
} >>- reloadMembers(study)
|
||||
} >>- reloadMembers(study) >>- indexStudy(study)
|
||||
}
|
||||
}
|
||||
|
||||
def kick(studyId: Study.ID, userId: User.ID) = sequenceStudy(studyId) { study =>
|
||||
study.isMember(userId) ?? {
|
||||
studyRepo.removeMember(study, userId)
|
||||
} >>- reloadMembers(study)
|
||||
} >>- reloadMembers(study) >>- indexStudy(study)
|
||||
}
|
||||
|
||||
def setShapes(userId: User.ID, studyId: Study.ID, position: Position.Ref, shapes: Shapes, uid: Uid) = sequenceStudy(studyId) { study =>
|
||||
|
@ -188,7 +190,8 @@ final class StudyApi(
|
|||
studyRepo.updateNow(study)
|
||||
newChapter.root.nodeAt(position.path).flatMap(_.comments findBy comment.by) ?? { c =>
|
||||
chapterRepo.update(newChapter) >>-
|
||||
sendTo(study, Socket.SetComment(position, c, uid))
|
||||
sendTo(study, Socket.SetComment(position, c, uid)) >>-
|
||||
indexStudy(study)
|
||||
}
|
||||
case None => fufail(s"Invalid setComment $studyId $position") >>- reloadUid(study, uid)
|
||||
}
|
||||
|
@ -203,7 +206,8 @@ final class StudyApi(
|
|||
chapter.deleteComment(id, position.path) match {
|
||||
case Some(newChapter) =>
|
||||
chapterRepo.update(newChapter) >>-
|
||||
sendTo(study, Socket.DeleteComment(position, id, uid))
|
||||
sendTo(study, Socket.DeleteComment(position, id, uid)) >>-
|
||||
indexStudy(study)
|
||||
case None => fufail(s"Invalid deleteComment $studyId $position $id") >>- reloadUid(study, uid)
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +241,8 @@ final class StudyApi(
|
|||
}
|
||||
} >> chapterRepo.insert(chapter) >>
|
||||
doSetChapter(byUserId, study, chapter.id, socket, uid) >>-
|
||||
studyRepo.updateNow(study)
|
||||
studyRepo.updateNow(study) >>-
|
||||
indexStudy(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -288,7 +293,7 @@ final class StudyApi(
|
|||
reloadChapters(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
} >>- indexStudy(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +310,7 @@ final class StudyApi(
|
|||
} >> chapterRepo.delete(chapter.id)
|
||||
case _ => funit
|
||||
} >>- reloadChapters(study)
|
||||
}
|
||||
} >>- indexStudy(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -323,13 +328,17 @@ final class StudyApi(
|
|||
settings = settings,
|
||||
visibility = data.vis)
|
||||
(newStudy != study) ?? {
|
||||
studyRepo.updateSomeFields(newStudy) >>- sendTo(study, Socket.ReloadAll)
|
||||
studyRepo.updateSomeFields(newStudy) >>-
|
||||
sendTo(study, Socket.ReloadAll) >>-
|
||||
indexStudy(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def delete(study: Study) = sequenceStudy(study.id) { study =>
|
||||
studyRepo.delete(study) >> chapterRepo.deleteByStudy(study)
|
||||
studyRepo.delete(study) >>
|
||||
chapterRepo.deleteByStudy(study) >>-
|
||||
(indexer ! actorApi.RemoveStudy(study.id))
|
||||
}
|
||||
|
||||
def like(studyId: Study.ID, userId: User.ID, v: Boolean, socket: ActorRef, uid: Uid): Funit =
|
||||
|
@ -344,6 +353,8 @@ final class StudyApi(
|
|||
|
||||
def resetAllRanks = studyRepo.resetAllRanks
|
||||
|
||||
private def indexStudy(study: Study) = indexer ! actorApi.SaveStudy(study)
|
||||
|
||||
private def reloadUid(study: Study, uid: Uid) =
|
||||
sendTo(study, Socket.ReloadUid(uid))
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package lila.study
|
||||
package actorApi
|
||||
|
||||
case class SaveStudy(study: Study)
|
||||
case class RemoveStudy(id: Study.ID)
|
|
@ -2,11 +2,14 @@ package lila.studySearch
|
|||
|
||||
import akka.actor._
|
||||
import com.typesafe.config.Config
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.common.paginator._
|
||||
import lila.db.dsl._
|
||||
import lila.hub.MultiThrottler
|
||||
import lila.search._
|
||||
import lila.search.PaginatorBuilder
|
||||
import lila.study.actorApi._
|
||||
import lila.study.Study
|
||||
import lila.user.User
|
||||
|
||||
|
@ -22,7 +25,16 @@ final class Env(
|
|||
|
||||
private val client = makeClient(Index(IndexName))
|
||||
|
||||
val api = new StudySearchApi(client, studyEnv.studyRepo, studyEnv.chapterRepo)
|
||||
private val indexThrottler = system.actorOf(Props(new MultiThrottler(
|
||||
executionTimeout = 2.seconds.some,
|
||||
logger = logger)
|
||||
))
|
||||
|
||||
val api = new StudySearchApi(
|
||||
client = client,
|
||||
indexThrottler = indexThrottler,
|
||||
studyEnv.studyRepo,
|
||||
studyEnv.chapterRepo)
|
||||
|
||||
def apply(me: Option[User])(text: String, page: Int) =
|
||||
Paginator[Study.WithChaptersAndLiked](
|
||||
|
@ -39,13 +51,13 @@ final class Env(
|
|||
}
|
||||
}
|
||||
|
||||
// system.actorOf(Props(new Actor {
|
||||
// import lila.study.actorApi._
|
||||
// def receive = {
|
||||
// case InsertTeam(team) => api store team
|
||||
// case RemoveTeam(id) => client deleteById Id(id)
|
||||
// }
|
||||
// }), name = ActorName)
|
||||
system.actorOf(Props(new Actor {
|
||||
import lila.study.actorApi._
|
||||
def receive = {
|
||||
case SaveStudy(study) => api store study
|
||||
case RemoveStudy(id) => client deleteById Id(id)
|
||||
}
|
||||
}), name = ActorName)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
|
|
@ -4,11 +4,12 @@ private[studySearch] object Fields {
|
|||
val name = "name"
|
||||
val owner = "owner"
|
||||
val members = "members"
|
||||
val chapters = "chapters"
|
||||
val chapterNames = "chapterNames"
|
||||
val chapterTexts = "chapterTexts"
|
||||
// val createdAt = "createdAt"
|
||||
// val updatedAt = "updatedAt"
|
||||
// val rank = "rank"
|
||||
val likes = "likes"
|
||||
// val likes = "likes"
|
||||
val public = "public"
|
||||
|
||||
object chapter {
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package lila.studySearch
|
||||
|
||||
import lila.search._
|
||||
import lila.study.{ Study, Chapter, StudyRepo, ChapterRepo }
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import play.api.libs.iteratee._
|
||||
import play.api.libs.json._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import lila.hub.MultiThrottler
|
||||
import lila.search._
|
||||
import lila.socket.tree.Node.{ Comments, Comment }
|
||||
import lila.study.{ Study, Chapter, StudyRepo, ChapterRepo, RootOrNode }
|
||||
|
||||
final class StudySearchApi(
|
||||
client: ESClient,
|
||||
indexThrottler: ActorRef,
|
||||
studyRepo: StudyRepo,
|
||||
chapterRepo: ChapterRepo) extends SearchReadApi[Study, Query] {
|
||||
|
||||
|
@ -18,34 +23,52 @@ final class StudySearchApi(
|
|||
|
||||
def count(query: Query) = client.count(query) map (_.count)
|
||||
|
||||
def store(study: Study) = getChapters(study) flatMap { s =>
|
||||
client.store(Id(s.study.id), toDoc(s))
|
||||
def store(study: Study) = fuccess {
|
||||
indexThrottler ! MultiThrottler.work(
|
||||
id = study.id,
|
||||
run = studyRepo byId study.id flatMap { _ ?? doStore },
|
||||
delay = 5.seconds.some)
|
||||
}
|
||||
|
||||
private def doStore(study: Study) =
|
||||
getChapters(study) flatMap { s =>
|
||||
client.store(Id(s.study.id), toDoc(s))
|
||||
}
|
||||
|
||||
private def toDoc(s: Study.WithActualChapters) = Json.obj(
|
||||
Fields.name -> s.study.name,
|
||||
Fields.owner -> s.study.ownerId,
|
||||
Fields.members -> s.study.members.ids,
|
||||
Fields.chapters -> JsArray(s.chapters.map(chapterToDoc)),
|
||||
Fields.chapterNames -> s.chapters.collect {
|
||||
case c if !Chapter.isDefaultName(c.name) => c.name
|
||||
}.mkString(" "),
|
||||
Fields.chapterTexts -> noMultiSpace(s.chapters.map(chapterText).mkString(" ")),
|
||||
// Fields.createdAt -> study.createdAt)
|
||||
// Fields.updatedAt -> study.updatedAt,
|
||||
// Fields.rank -> study.rank,
|
||||
Fields.public -> s.study.isPublic,
|
||||
Fields.likes -> s.study.likes.value)
|
||||
Fields.public -> s.study.isPublic)
|
||||
|
||||
private def chapterToDoc(c: Chapter) = Json.obj(
|
||||
Fields.chapter.name -> c.name)
|
||||
private def chapterText(c: Chapter): String = nodeText(c.root)
|
||||
|
||||
private def nodeText(n: RootOrNode): String =
|
||||
commentsText(n.comments) + " " + n.children.nodes.map(nodeText).mkString(" ")
|
||||
|
||||
private def commentsText(cs: Comments): String =
|
||||
cs.value.map(_.text.value) mkString " "
|
||||
|
||||
private def getChapters(s: Study): Fu[Study.WithActualChapters] =
|
||||
chapterRepo.orderedByStudy(s.id) map { Study.WithActualChapters(s, _) }
|
||||
|
||||
private val multiSpaceRegex = """\s{2,}""".r
|
||||
private def noMultiSpace(text: String) = multiSpaceRegex.replaceAllIn(text, " ")
|
||||
|
||||
def reset = client match {
|
||||
case c: ESClientHttp => c.putMapping >> {
|
||||
lila.log("studySearch").info(s"Index to ${c.index.name}")
|
||||
import lila.db.dsl._
|
||||
studyRepo.cursor($empty).enumerate() |>>>
|
||||
Iteratee.foldM[Study, Unit](()) {
|
||||
case (_, study) => store(study)
|
||||
case (_, study) => doStore(study)
|
||||
}
|
||||
}
|
||||
case _ => funit
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
package lila
|
||||
|
||||
package object studySearch extends PackageObject with WithPlay
|
||||
package object studySearch extends PackageObject with WithPlay {
|
||||
|
||||
private[studySearch] val logger = lila.log("studySearch")
|
||||
}
|
||||
|
|
|
@ -534,6 +534,3 @@ div.undertable_inner {
|
|||
height: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
.studies #infscr-loading {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,13 @@
|
|||
color: #759900;
|
||||
font-size: 27px;
|
||||
}
|
||||
.studies .top .search input {
|
||||
margin-right: 20px;
|
||||
padding: 0 10px;
|
||||
font-size: 15px;
|
||||
line-height: 28px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.studies .list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
@ -165,3 +172,6 @@ body.dark .studies .study {
|
|||
background: #3893E8;
|
||||
color: #fff;
|
||||
}
|
||||
.studies #infscr-loading {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue