From 77a7dfd3ac14f72b44bed69beb785d7e7f2c6696 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Wed, 8 May 2024 21:34:57 +0700 Subject: [PATCH] Implement forum endpoints --- build.sbt | 4 ++- .../smithy/{forum.smithy => search.smithy} | 8 +++--- modules/app/src/main/resources/logback.xml | 22 +++++++++++++++ .../app/src/main/scala/app.rersources.scala | 21 +++++++++++++++ modules/app/src/main/scala/app.scala | 9 ++++--- .../app/src/main/scala/http.middleware.scala | 5 +++- modules/app/src/main/scala/http.routes.scala | 4 +-- .../app/src/main/scala/service.search.scala | 27 +++++++++++++++++++ modules/core/src/main/scala/ESClient.scala | 22 +++++---------- .../{QueryParser.scala => JsonParser.scala} | 0 10 files changed, 95 insertions(+), 27 deletions(-) rename modules/api/src/main/smithy/{forum.smithy => search.smithy} (86%) create mode 100644 modules/app/src/main/resources/logback.xml create mode 100644 modules/app/src/main/scala/app.rersources.scala create mode 100644 modules/app/src/main/scala/service.search.scala rename play/app/src/main/scala/{QueryParser.scala => JsonParser.scala} (100%) diff --git a/build.sbt b/build.sbt index 93ea84f1..701e47a1 100644 --- a/build.sbt +++ b/build.sbt @@ -75,15 +75,17 @@ lazy val app = (project in file("modules/app")) libraryDependencies ++= Seq( "com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value, "com.disneystreaming.smithy4s" %% "smithy4s-http4s-swagger" % smithy4sVersion.value, + "com.sksamuel.elastic4s" %% "elastic4s-effect-cats" % "8.11.5", catsCore, catsEffect, + ducktape, http4sServer, http4sEmberClient, cirisCore, cirisHtt4s, logbackX ), - excludeDependencies ++= Seq("org.typelevel" % "cats-core_2.13", "org.typelevel" % "cats-kernel_2.13"), + excludeDependencies ++= Seq("org.typelevel" % "cats-core_2.13", "org.typelevel" % "cats-kernel_2.13", "com.sksamuel.elastic4s" % "elastic4s-core_2.13", "com.sksamuel.elastic4s" % "elastic4s-domain_2.13", "com.sksamuel.elastic4s" % "elastic4s-http_2.13", "com.fasterxml.jackson.module" % "jackson-module-scala_2.13"), Compile / run / fork := true, ) .enablePlugins(JavaAppPackaging) diff --git a/modules/api/src/main/smithy/forum.smithy b/modules/api/src/main/smithy/search.smithy similarity index 86% rename from modules/api/src/main/smithy/forum.smithy rename to modules/api/src/main/smithy/search.smithy index 0274fa22..d0cf3758 100644 --- a/modules/api/src/main/smithy/forum.smithy +++ b/modules/api/src/main/smithy/search.smithy @@ -7,7 +7,7 @@ use alloy#simpleRestJson @simpleRestJson service SearchService { version: "3.0.0", - operations: [SearchForum] + operations: [SearchForum, CountForum] } @readonly @@ -41,6 +41,8 @@ structure SearchForumInput { } structure ForumInputBody { - troll: Boolean - query: String + @required + text: String + @required + troll: Boolean = false } diff --git a/modules/app/src/main/resources/logback.xml b/modules/app/src/main/resources/logback.xml new file mode 100644 index 00000000..0c41428a --- /dev/null +++ b/modules/app/src/main/resources/logback.xml @@ -0,0 +1,22 @@ + + + + logs/application.log + + %date [%thread] %-5level %logger{20} - %msg%n%xException + + + + + + %date [%thread] %-5level %logger{20} - %msg%n%xException + + + + + + + + + + diff --git a/modules/app/src/main/scala/app.rersources.scala b/modules/app/src/main/scala/app.rersources.scala new file mode 100644 index 00000000..256e5002 --- /dev/null +++ b/modules/app/src/main/scala/app.rersources.scala @@ -0,0 +1,21 @@ +package lila.search +package app + +import cats.effect.* +import org.typelevel.log4cats.Logger +import com.sksamuel.elastic4s.http.JavaClient +import com.sksamuel.elastic4s.cats.effect.instances.* +import com.sksamuel.elastic4s.{ ElasticClient, ElasticProperties } + +class AppResources private (val esClient: ESClient[IO]) + +object AppResources: + + def instance(conf: AppConfig)(using Logger[IO]): Resource[IO, AppResources] = + makeClient(conf.elastic) + .map(ESClient.apply[IO]) + .map(AppResources(_)) + + def makeClient(conf: ElasticConfig): Resource[IO, ElasticClient] = + Resource.make(IO(ElasticClient(JavaClient(ElasticProperties(conf.uri))))): client => + IO(client.close()) diff --git a/modules/app/src/main/scala/app.scala b/modules/app/src/main/scala/app.scala index caaca7a9..f1e37a4e 100644 --- a/modules/app/src/main/scala/app.scala +++ b/modules/app/src/main/scala/app.scala @@ -14,14 +14,15 @@ object App extends IOApp.Simple: def app: Resource[IO, Unit] = for config <- AppConfig.load.toResource - _ <- Logger[IO].info(s"Starting fide-api with config: $config").toResource - _ <- SearchApp(config).run() + _ <- Logger[IO].info(s"Starting lila-search with config: $config").toResource + res <- AppResources.instance(config) + _ <- SearchApp(res, config).run() yield () -class SearchApp(config: AppConfig)(using Logger[IO]): +class SearchApp(res: AppResources, config: AppConfig)(using Logger[IO]): def run(): Resource[IO, Unit] = for - httpApp <- Routes + httpApp <- Routes(res) server <- MkHttpServer.apply.newEmber(config.server, httpApp) _ <- Logger[IO].info(s"Starting server on ${config.server.host}:${config.server.port}").toResource yield () diff --git a/modules/app/src/main/scala/http.middleware.scala b/modules/app/src/main/scala/http.middleware.scala index 18cdede7..9bc8c6ef 100644 --- a/modules/app/src/main/scala/http.middleware.scala +++ b/modules/app/src/main/scala/http.middleware.scala @@ -15,5 +15,8 @@ def ApplyMiddleware(routes: HttpRoutes[IO]): HttpApp[IO] = val timeout: Middleware = Timeout(60.seconds) val middleware = autoSlash.andThen(timeout) + val logger = + RequestLogger.httpApp[IO](true, true) andThen + ResponseLogger.httpApp[IO, Request[IO]](true, true) - middleware(routes).orNotFound + logger(middleware(routes).orNotFound) diff --git a/modules/app/src/main/scala/http.routes.scala b/modules/app/src/main/scala/http.routes.scala index 5ab235ce..86bd7edd 100644 --- a/modules/app/src/main/scala/http.routes.scala +++ b/modules/app/src/main/scala/http.routes.scala @@ -9,11 +9,11 @@ import org.http4s.{ HttpApp, HttpRoutes } import org.typelevel.log4cats.Logger import smithy4s.http4s.SimpleRestJsonBuilder -def Routes(using Logger[IO]): Resource[IO, HttpApp[IO]] = +def Routes(resources: AppResources)(using Logger[IO]): Resource[IO, HttpApp[IO]] = val healthServiceImpl: HealthService[IO] = new HealthService.Default[IO](IO.stub) - val searchServiceImpl: SearchService[IO] = new SearchService.Default[IO](IO.stub) + val searchServiceImpl: SearchService[IO] = SearchServiceImpl(resources.esClient) val search: Resource[IO, HttpRoutes[IO]] = SimpleRestJsonBuilder.routes(searchServiceImpl).resource diff --git a/modules/app/src/main/scala/service.search.scala b/modules/app/src/main/scala/service.search.scala new file mode 100644 index 00000000..0e7fb3ed --- /dev/null +++ b/modules/app/src/main/scala/service.search.scala @@ -0,0 +1,27 @@ +package lila.search +package app + +import cats.effect.* +import lila.search.spec.* +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.syntax.* +import forum.ForumQuery.* +import io.github.arainko.ducktape.* + +class SearchServiceImpl(esClient: ESClient[IO])(using Logger[IO]) extends SearchService[IO]: + + override def countForum(text: String, troll: Boolean): IO[CountResponse] = + esClient + .count(Index("forum"), Forum(text, troll)) + .map(_.to[CountResponse]) + .handleErrorWith: e => + error"Error in countForum: text=$text, troll=$troll" *> + IO.raiseError(InternalServerError("Internal server error")) + + override def searchForum(body: ForumInputBody, from: Int, size: Int): IO[SearchResponse] = + esClient + .search(Index("forum"), Forum(body.text, body.troll), From(from), Size(size)) + .map(_.to[SearchResponse]) + .handleErrorWith: e => + error"Error in searchForum: body=$body, from=$from, size=$size" *> + IO.raiseError(InternalServerError("Internal server error")) diff --git a/modules/core/src/main/scala/ESClient.scala b/modules/core/src/main/scala/ESClient.scala index 45e66909..cb81403b 100644 --- a/modules/core/src/main/scala/ESClient.scala +++ b/modules/core/src/main/scala/ESClient.scala @@ -41,22 +41,18 @@ object ESClient { q: Queryable[A] ): F[SearchResponse] = client - .execute { q.searchDef(query)(from, size)(index) } + .execute(q.searchDef(query)(from, size)(index)) .flatMap(toResult) .map(SearchResponse.apply) def count[A](index: Index, query: A)(implicit q: Queryable[A]): F[CountResponse] = client - .execute { - q.countDef(query)(index) - } + .execute(q.countDef(query)(index)) .flatMap(toResult) .map(CountResponse.apply) def store(index: Index, id: Id, obj: JsonObject): F[Response[IndexResponse]] = - client.execute { - indexInto(index.name).source(obj.json).id(id.value) - } + client.execute(indexInto(index.name).source(obj.json).id(id.value)) def storeBulk(index: Index, objs: List[(String, JsonObject)]): F[Unit] = if (objs.isEmpty) ().pure[F] @@ -70,9 +66,7 @@ object ESClient { }.void def deleteOne(index: Index, id: Id): F[Response[DeleteResponse]] = - client.execute { - deleteById(index.toES, id.value) - } + client.execute(deleteById(index.toES, id.value)) def deleteMany(index: Index, ids: List[Id]): F[Response[BulkResponse]] = client.execute { @@ -96,17 +90,13 @@ object ESClient { def refreshIndex(index: Index): F[Unit] = client - .execute { - ElasticDsl.refreshIndex(index.name) - } + .execute(ElasticDsl.refreshIndex(index.name)) .void .recover { case _: Exception => println(s"Failed to refresh index $index") } private def dropIndex(index: Index) = - client.execute { - deleteIndex(index.name) - } + client.execute { deleteIndex(index.name) } } } diff --git a/play/app/src/main/scala/QueryParser.scala b/play/app/src/main/scala/JsonParser.scala similarity index 100% rename from play/app/src/main/scala/QueryParser.scala rename to play/app/src/main/scala/JsonParser.scala