more study search engine WIP

proxy-selector
Thibault Duplessis 2016-07-25 22:34:15 +02:00
parent a715691076
commit f55da1c9a7
17 changed files with 169 additions and 45 deletions

View File

@ -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")
}

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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)
}

View File

@ -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")

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -0,0 +1,5 @@
package lila.study
package actorApi
case class SaveStudy(study: Study)
case class RemoveStudy(id: Study.ID)

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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")
}

View File

@ -534,6 +534,3 @@ div.undertable_inner {
height: auto;
max-height: 200px;
}
.studies #infscr-loading {
width: 100%;
}

View File

@ -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%;
}