diff --git a/build.sbt b/build.sbt index 1660ad56..ea6604e3 100644 --- a/build.sbt +++ b/build.sbt @@ -29,11 +29,13 @@ val commonSettings = Seq( lazy val core = project .in(file("modules/core")) + .enablePlugins(Smithy4sCodegenPlugin) .settings( name := "core", commonSettings, libraryDependencies ++= Seq( - catsCore + catsCore, + smithy4sCore ) ) @@ -54,7 +56,8 @@ lazy val elastic = project ) .dependsOn(core) -lazy val api = (project in file("modules/api")) +lazy val api = project + .in(file("modules/api")) .enablePlugins(Smithy4sCodegenPlugin) .settings( name := "api", @@ -67,7 +70,9 @@ lazy val api = (project in file("modules/api")) ) .dependsOn(core) -lazy val ingestor = (project in file("modules/ingestor")) +lazy val ingestor = project + .in(file("modules/ingestor")) + .enablePlugins(Smithy4sCodegenPlugin) .settings( name := "ingestor", commonSettings, @@ -83,6 +88,7 @@ lazy val ingestor = (project in file("modules/ingestor")) declineCatsEffect, ducktape, cirisCore, + smithy4sCore, smithy4sJson, jsoniterCore, jsoniterMacro, @@ -102,9 +108,10 @@ lazy val ingestor = (project in file("modules/ingestor")) Compile / run / fork := true ) .enablePlugins(JavaAppPackaging) - .dependsOn(elastic, api) + .dependsOn(elastic, core) -lazy val client = (project in file("modules/client")) +lazy val client = project + .in(file("modules/client")) .settings( name := "client", commonSettings, @@ -117,7 +124,8 @@ lazy val client = (project in file("modules/client")) ) .dependsOn(api, core) -lazy val app = (project in file("modules/app")) +lazy val app = project + .in(file("modules/app")) .settings( name := "lila-search", commonSettings, @@ -139,22 +147,21 @@ lazy val app = (project in file("modules/app")) logback, otel4sMetricts, otel4sSdk, - otel4sPrometheusExporter, - weaver, - testContainers + otel4sPrometheusExporter ), Compile / run / fork := true ) .enablePlugins(JavaAppPackaging) .dependsOn(api, elastic) -val e2e = (project in file("modules/e2e")) +val e2e = project + .in(file("modules/e2e")) .settings( publish := {}, publish / skip := true, - libraryDependencies ++= Seq(weaver) + libraryDependencies ++= Seq(testContainers, weaver) ) - .dependsOn(client, app) + .dependsOn(client, app, ingestor) lazy val root = project .in(file(".")) diff --git a/modules/api/src/main/smithy/search.smithy b/modules/api/src/main/smithy/search.smithy index 5cf8183f..0dd78674 100644 --- a/modules/api/src/main/smithy/search.smithy +++ b/modules/api/src/main/smithy/search.smithy @@ -7,10 +7,18 @@ use smithy4s.meta#adt use smithy.api#default use smithy.api#jsonName +use lila.search.core#Ids +use lila.search.core#FromInt +use lila.search.core#SizeInt +use lila.search.core#PlayerIds +use lila.search.core#Strings +use lila.search.core#IndexString +use lila.search.core#DateTime + @simpleRestJson service SearchService { version: "3.0.0" - operations: [Search, Count, DeleteById, DeleteByIds, Mapping, Refresh, Store, StoreBulkForum, StoreBulkGame, StoreBulkStudy, StoreBulkTeam] + operations: [Search, Count] } @readonly @@ -55,155 +63,6 @@ operation Count { errors: [InternalServerError] } -@http(method: "POST", uri: "/api/delete/id/{index}/{id}", code: 200) -operation DeleteById { - input := { - @required - @httpLabel - index: IndexString - - @required - @httpLabel - id: String - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/delete/ids/{index}", code: 200) -operation DeleteByIds { - input := { - @required - @httpLabel - index: IndexString - - @required - ids: Ids - } - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/mapping/{index}", code: 200) -operation Mapping { - input := { - @required - @httpLabel - index: IndexString - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/refresh/{index}", code: 200) -operation Refresh { - input := { - @required - @httpLabel - index: IndexString - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/store/{id}", code: 200) -operation Store { - input := { - - @httpLabel - @required - id: String - - @required - source: Source - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/store-bulk/forum", code: 200) -operation StoreBulkForum { - input := { - @required - sources: ForumSources - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/store-bulk/game", code: 200) -operation StoreBulkGame { - input := { - @required - sources: GameSources - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/store-bulk/study", code: 200) -operation StoreBulkStudy { - input := { - @required - sources: StudySources - } - - errors: [InternalServerError] -} - -@http(method: "POST", uri: "/api/store-bulk/team", code: 200) -operation StoreBulkTeam { - input := { - @required - sources: TeamSources - } - - errors: [InternalServerError] -} - -list ForumSources { - member: ForumSourceWithId -} - -list GameSources { - member: GameSourceWithId -} - -list StudySources { - member: StudySourceWithId -} - -list TeamSources { - member: TeamSourceWithId -} - -structure ForumSourceWithId { - @required - id: String - @required - source: ForumSource -} - -structure TeamSourceWithId { - @required - id: String - @required - source: TeamSource -} - -structure StudySourceWithId { - @required - id: String - @required - source: StudySource -} - -structure GameSourceWithId { - @required - id: String - @required - source: GameSource -} - structure Forum { @required text: String @@ -282,108 +141,10 @@ union Query { team: Team } -structure ForumSource { - @required - @jsonName("bo") - body: String - @required - @jsonName("to") - topic: String - @required - @jsonName("ti") - topicId: String - @jsonName("au") - author: String - @required - @jsonName("tr") - troll: Boolean - /// time in milliseconds - @required - @jsonName("da") - date: Long -} - -structure GameSource { - @required - @jsonName("s") - status: Integer - @required - @jsonName("t") - turns: Integer - @required - @jsonName("r") - rated: Boolean - @required - @jsonName("p") - perf: Integer - @jsonName("u") - uids: PlayerIds - @jsonName("w") - winner: String - @jsonName("o") - loser: String +@error("server") +@httpError(500) +structure InternalServerError { @required - @jsonName("c") - winnerColor: Integer - @jsonName("a") - averageRating: Integer - @jsonName("i") - ai: Integer - @required - @jsonName("d") - date: DateTime - @jsonName("l") - duration: Integer - @jsonName("ct") - clockInit: Integer - @jsonName("ci") - clockInc: Integer - @required - @jsonName("n") - analysed: Boolean - @jsonName("wu") - whiteUser: String - @jsonName("bu") - blackUser: String - @jsonName("so") - source: Integer + message: String } -structure StudySource { - @required - name: String - @required - owner: String - @required - members: PlayerIds - @required - chapterNames: String - - @required - chapterTexts: String - @default - topics: Strings - @required - likes: Integer - @required - public: Boolean -} - -structure TeamSource { - @required - @jsonName("na") - name: String - @required - @jsonName("de") - description: String - @required - @jsonName("nbm") - nbMembers: Integer -} - -union Source { - forum: ForumSource - game: GameSource - study: StudySource - team: TeamSource -} diff --git a/modules/app/src/main/scala/service.search.scala b/modules/app/src/main/scala/service.search.scala index 4094a6b1..0b8a5faf 100644 --- a/modules/app/src/main/scala/service.search.scala +++ b/modules/app/src/main/scala/service.search.scala @@ -17,85 +17,10 @@ import java.time.Instant class SearchServiceImpl(esClient: ESClient[IO])(using LoggerFactory[IO]) extends SearchService[IO]: - import SearchServiceImpl.{ given, * } + import SearchServiceImpl.given given logger: Logger[IO] = summon[LoggerFactory[IO]].getLogger - override def storeBulkTeam(sources: List[TeamSourceWithId]): IO[Unit] = - esClient - .storeBulk( - Index.Team, - sources.map(s => s.id -> s.source) - ) - .handleErrorWith: e => - logger.error(e)(s"Error in storeBulkTeam: sources=$sources") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def storeBulkStudy(sources: List[StudySourceWithId]): IO[Unit] = - esClient - .storeBulk( - Index.Study, - sources.map(s => s.id -> s.source) - ) - .handleErrorWith: e => - logger.error(e)(s"Error in storeBulkStudy: sources=$sources") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def storeBulkGame(sources: List[GameSourceWithId]): IO[Unit] = - esClient - .storeBulk( - Index.Game, - sources.map(s => s.id -> s.source) - ) - .handleErrorWith: e => - logger.error(e)(s"Error in storeBulkGame: sources=$sources") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def storeBulkForum(sources: List[ForumSourceWithId]): IO[Unit] = - esClient - .storeBulk( - Index.Forum, - sources.map(s => s.id -> s.source) - ) - .handleErrorWith: e => - logger.error(e)(s"Error in storeBulkForum: sources=$sources") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def store(id: String, source: Source): IO[Unit] = - esClient - .store(source.index, Id(id), source) - .handleErrorWith: e => - logger.error(e)(s"Error in store: source=$source, id=$id") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def refresh(index: Index): IO[Unit] = - esClient - .refreshIndex(index) - .handleErrorWith: e => - logger.error(e)(s"Error in refresh: index=$index") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def mapping(index: Index): IO[Unit] = - esClient - .putMapping(index) - .handleErrorWith: e => - logger.error(e)(s"Error in mapping: index=$index") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def deleteById(index: Index, id: String): IO[Unit] = - esClient - .deleteOne(index, Id(id)) - .handleErrorWith: e => - logger.error(e)(s"Error in deleteById: index=$index, id=$id") *> - IO.raiseError(InternalServerError("Internal server error")) - - override def deleteByIds(index: Index, ids: List[Id]): IO[Unit] = - esClient - .deleteMany(index, ids) - .handleErrorWith: e => - logger.error(e)(s"Error in deleteByIds: index=$index, ids=$ids") *> - IO.raiseError(InternalServerError("Internal server error")) - override def count(query: Query): IO[CountOutput] = esClient .count(query) @@ -148,16 +73,3 @@ object SearchServiceImpl: import com.github.plokhotnyuk.jsoniter_scala.core.* given [A: Schema]: Indexable[A] = (a: A) => writeToString(a) - given Indexable[Source] = - _ match - case f: Source.ForumCase => writeToString(f.forum) - case g: Source.GameCase => writeToString(g.game) - case s: Source.StudyCase => writeToString(s.study) - case t: Source.TeamCase => writeToString(t.team) - - extension (source: Source) - def index = source match - case s: Source.ForumCase => Index.Forum - case s: Source.GameCase => Index.Game - case s: Source.StudyCase => Index.Study - case s: Source.TeamCase => Index.Team diff --git a/modules/client/src/main/scala/NoopSearchClient.scala b/modules/client/src/main/scala/NoopSearchClient.scala index 1ef31801..ff49bdd5 100644 --- a/modules/client/src/main/scala/NoopSearchClient.scala +++ b/modules/client/src/main/scala/NoopSearchClient.scala @@ -7,25 +7,7 @@ import scala.concurrent.Future object NoopSearchClient extends SearchClient: - override def refresh(index: Index): Future[Unit] = Future.successful(()) - - override def storeBulkTeam(sources: List[TeamSourceWithId]): Future[Unit] = Future.successful(()) - - override def deleteByIds(index: Index, ids: List[Id]): Future[Unit] = Future.successful(()) - - override def storeBulkForum(sources: List[ForumSourceWithId]): Future[Unit] = Future.successful(()) - - override def deleteById(index: Index, id: String): Future[Unit] = Future.successful(()) - override def search(query: Query, from: From, size: Size): Future[SearchOutput] = Future.successful(SearchOutput(Nil)) - override def store(id: String, source: Source): Future[Unit] = Future.successful(()) - - override def storeBulkGame(sources: List[GameSourceWithId]): Future[Unit] = Future.successful(()) - - override def storeBulkStudy(sources: List[StudySourceWithId]): Future[Unit] = Future.successful(()) - override def count(query: Query): Future[CountOutput] = Future.successful(CountOutput(0)) - - override def mapping(index: Index): Future[Unit] = Future.successful(()) diff --git a/modules/client/src/main/scala/PlaySearchClient.scala b/modules/client/src/main/scala/PlaySearchClient.scala index 4c388427..7cc13569 100644 --- a/modules/client/src/main/scala/PlaySearchClient.scala +++ b/modules/client/src/main/scala/PlaySearchClient.scala @@ -28,33 +28,6 @@ class PlaySearchClient(client: StandaloneWSClient, baseUrl: String)(using Execut import implicits.given - override def storeBulkTeam(sources: List[TeamSourceWithId]): Future[Unit] = - request_(s"$baseUrl/store-bulk/team", StoreBulkTeamInput(sources)) - - override def storeBulkStudy(sources: List[StudySourceWithId]): Future[Unit] = - request_(s"$baseUrl/store-bulk/study", StoreBulkStudyInput(sources)) - - override def storeBulkGame(sources: List[GameSourceWithId]): Future[Unit] = - request_(s"$baseUrl/store-bulk/game", StoreBulkGameInput(sources)) - - override def storeBulkForum(sources: List[ForumSourceWithId]): Future[Unit] = - request_(s"$baseUrl/store-bulk/forum", StoreBulkForumInput(sources)) - - override def store(id: String, source: Source): Future[Unit] = - request_(s"$baseUrl/store/$id", SourceInput(source)) - - override def refresh(index: Index): Future[Unit] = - request_(s"$baseUrl/refresh/${index.value}") - - override def mapping(index: Index): Future[Unit] = - request_(s"$baseUrl/mapping/${index.value}") - - override def deleteById(index: Index, id: String): Future[Unit] = - request_(s"$baseUrl/delete/id/${index.value}/$id") - - override def deleteByIds(index: Index, ids: List[Id]): Future[Unit] = - request_(s"$baseUrl/delete/ids/${index.value}", IdsInput(ids)) - override def count(query: Query): Future[CountOutput] = request(s"$baseUrl/count", SearchInput(query)) @@ -73,30 +46,7 @@ class PlaySearchClient(client: StandaloneWSClient, baseUrl: String)(using Execut case res => Future.failed(SearchError.InternalServerError(s"$url ${res.status} ${res.body}")) catch case e: JsonWriterException => Future.failed(SearchError.JsonWriterError(e.toString)) - private def request_[D: Schema](url: String, data: D): Future[Unit] = - try - client - .url(url) - .post(data) - .flatMap: - case res if res.status == 200 => Future.successful(()) - case res if res.status == 400 => - Future.failed(SearchError.BadRequest(s"$url ${res.status} ${res.body}")) - case res => Future.failed(SearchError.InternalServerError(s"$url ${res.status} ${res.body}")) - catch case e: JsonWriterException => Future.failed(SearchError.JsonWriterError(e.toString)) - - private def request_(url: String): Future[Unit] = - client - .url(url) - .execute("POST") - .flatMap: - case res if res.status == 200 => Future.successful(()) - case res if res.status == 400 => - Future.failed(SearchError.BadRequest(s"$url ${res.status} ${res.body}")) - case res => Future.failed(SearchError.InternalServerError(s"$url ${res.status} ${res.body}")) - final private case class SearchInput(query: Query) -final private case class SourceInput(source: Source) final private case class IdsInput(ids: List[Id]) object implicits: @@ -107,14 +57,6 @@ object implicits: Query.schema.required[SearchInput]("query", _.query) )(SearchInput.apply) - given Schema[SourceInput] = struct( - Source.schema.required[SourceInput]("source", _.source) - )(SourceInput.apply) - - given Schema[IdsInput] = struct( - Ids.schema.required[IdsInput]("ids", x => Ids(x.ids)) - )(x => IdsInput(x.value)) - given [A](using JsonCodec[A]): BodyWritable[A] = BodyWritable(a => InMemoryBody(ByteString.fromArrayUnsafe(writeToArray(a))), "application/json") diff --git a/modules/client/src/main/scala/SearchClient.scala b/modules/client/src/main/scala/SearchClient.scala index 3c3c37d6..ba214143 100644 --- a/modules/client/src/main/scala/SearchClient.scala +++ b/modules/client/src/main/scala/SearchClient.scala @@ -4,38 +4,9 @@ package client import lila.search.spec.* import play.api.libs.ws.StandaloneWSClient -import scala.annotation.targetName import scala.concurrent.{ ExecutionContext, Future } -trait SearchClient extends SearchService[Future]: - client => - @targetName("storeBulkTeamWithPair") - def storeBulkTeam(sources: List[(String, TeamSource)]): Future[Unit] = - client.storeBulkTeam(sources.map(TeamSourceWithId.apply.tupled)) - - @targetName("storeBulkStudyWithPair") - def storeBulkStudy(sources: List[(String, StudySource)]): Future[Unit] = - client.storeBulkStudy(sources.map(StudySourceWithId.apply.tupled)) - - @targetName("storeBulkGameWithPair") - def storeBulkGame(sources: List[(String, GameSource)]): Future[Unit] = - client.storeBulkGame(sources.map(GameSourceWithId.apply.tupled)) - - @targetName("storeBulkForumWithPair") - def storeBulkForum(sources: List[(String, ForumSource)]): Future[Unit] = - client.storeBulkForum(sources.map(ForumSourceWithId.apply.tupled)) - - def storeForum(id: String, source: ForumSource): Future[Unit] = - client.store(id, Source.forum(source)) - - def storeGame(id: String, source: GameSource): Future[Unit] = - client.store(id, Source.game(source)) - - def storeStudy(id: String, source: StudySource): Future[Unit] = - client.store(id, Source.study(source)) - - def storeTeam(id: String, source: TeamSource): Future[Unit] = - client.store(id, Source.team(source)) +trait SearchClient extends SearchService[Future] object SearchClient: diff --git a/modules/api/src/main/scala/providers.scala b/modules/core/src/main/scala/providers.scala similarity index 98% rename from modules/api/src/main/scala/providers.scala rename to modules/core/src/main/scala/providers.scala index 829821b7..e1231b49 100644 --- a/modules/api/src/main/scala/providers.scala +++ b/modules/core/src/main/scala/providers.scala @@ -1,5 +1,5 @@ package lila.search -package spec +package core import cats.syntax.all.* import smithy4s.* diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/core/src/main/smithy/_global.smithy similarity index 69% rename from modules/api/src/main/smithy/_global.smithy rename to modules/core/src/main/smithy/_global.smithy index d5f0ae53..d7c18244 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/core/src/main/smithy/_global.smithy @@ -1,22 +1,15 @@ $version: "2" -namespace lila.search.spec +namespace lila.search.core use smithy4s.meta#unwrap use smithy4s.meta#refinement use alloy#simpleRestJson -@error("server") -@httpError(500) -structure InternalServerError { - @required - message: String -} - @trait(selector: "string") @refinement( targetType: "lila.search.Id" - providerImport: "lila.search.spec.providers.given" + providerImport: "lila.search.core.providers.given" ) structure IdFormat {} @@ -39,7 +32,7 @@ list PlayerIds { @trait(selector: "string") @refinement( targetType: "lila.search.SearchDateTime" - providerImport: "lila.search.spec.providers.given" + providerImport: "lila.search.core.providers.given" ) structure DateTimeFormat {} @@ -50,7 +43,7 @@ string DateTime @trait(selector: "integer") @refinement( targetType: "lila.search.Size" - providerImport: "lila.search.spec.providers.given" + providerImport: "lila.search.core.providers.given" ) structure SizeFormat {} @@ -61,7 +54,7 @@ integer SizeInt @trait(selector: "integer") @refinement( targetType: "lila.search.From" - providerImport: "lila.search.spec.providers.given" + providerImport: "lila.search.core.providers.given" ) structure FromFormat {} @@ -72,7 +65,7 @@ integer FromInt @trait(selector: "string") @refinement( targetType: "lila.search.Index" - providerImport: "lila.search.spec.providers.given" + providerImport: "lila.search.core.providers.given" ) structure IndexFormat {} diff --git a/modules/app/src/test/scala/Clients.scala b/modules/e2e/src/test/scala/Clients.scala similarity index 100% rename from modules/app/src/test/scala/Clients.scala rename to modules/e2e/src/test/scala/Clients.scala diff --git a/modules/e2e/src/test/scala/CompatSuite.scala b/modules/e2e/src/test/scala/CompatSuite.scala index 9552c504..bf728b8f 100644 --- a/modules/e2e/src/test/scala/CompatSuite.scala +++ b/modules/e2e/src/test/scala/CompatSuite.scala @@ -7,7 +7,7 @@ import com.comcast.ip4s.* import com.sksamuel.elastic4s.Indexable import lila.search.app.{ App, AppConfig, AppResources, ElasticConfig, HttpServerConfig } import lila.search.client.{ SearchClient, SearchError } -import lila.search.spec.{ CountOutput, Query, SearchOutput, Source } +import lila.search.spec.{ CountOutput, Query, SearchOutput } import org.typelevel.log4cats.noop.{ NoOpFactory, NoOpLogger } import org.typelevel.log4cats.{ Logger, LoggerFactory } import org.typelevel.otel4s.metrics.Meter @@ -16,7 +16,6 @@ import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter import play.api.libs.ws.* import play.api.libs.ws.ahc.* -import java.time.Instant import scala.concurrent.ExecutionContext.Implicits.* object CompatSuite extends weaver.IOSuite: @@ -61,80 +60,6 @@ object CompatSuite extends weaver.IOSuite: val query = Query.Team("foo") IO.fromFuture(IO(client.count(query))).map(expect.same(_, lila.search.spec.CountOutput(0))) - test("deleteById endpoint"): client => - IO.fromFuture(IO(client.deleteById(Index.Game, "iddddd"))).map(expect.same(_, ())) - - test("deleteByIds endpoint"): client => - IO.fromFuture(IO(client.deleteByIds(Index.Game, List(Id("a"), Id("b"), Id("c"))))).map(expect.same(_, ())) - - test("mapping endpoint"): client => - IO.fromFuture(IO(client.mapping(Index.Study))).map(expect.same(_, ())) - - test("refresh endpoint"): client => - IO.fromFuture(IO(client.refresh(Index.Forum))).map(expect.same(_, ())) - - test("store endpoint"): client => - val source = Source.team(lila.search.spec.TeamSource("names", "desc", 100)) - IO.fromFuture(IO(client.store("id", source))).map(expect.same(_, ())) - - test("store bulk forum endpoint"): client => - val now = Instant.now().toEpochMilli() - val sources = List( - lila.search.spec.ForumSourceWithId( - "id1", - lila.search.spec - .ForumSource("body1", "topic1", "topid1", true, now) - ), - lila.search.spec.ForumSourceWithId( - "id2", - lila.search.spec - .ForumSource("body2", "topic2", "topid2", true, now) - ) - ) - IO.fromFuture(IO(client.storeBulkForum(sources))).map(expect.same(_, ())) - - test("store bulk game endpoint"): client => - val now = SearchDateTime.fromInstant(Instant.now()) - val sources = List( - lila.search.spec.GameSourceWithId( - "id1", - lila.search.spec - .GameSource(1, 1, true, 4, 1, now, true) - ), - lila.search.spec.GameSourceWithId( - "id2", - lila.search.spec - .GameSource(2, 2, true, 4, 1, now, false) - ) - ) - IO.fromFuture(IO(client.storeBulkGame(sources))).map(expect.same(_, ())) - - test("store bulk study endpoint"): client => - val sources = List( - lila.search.spec.StudySourceWithId( - "id1", - lila.search.spec.StudySource("name1", "owner1", Nil, "chapter names", "chapter texts", 12, true) - ), - lila.search.spec.StudySourceWithId( - "id2", - lila.search.spec.StudySource("name2", "owner2", Nil, "chapter names", "chapter texts", 12, false) - ) - ) - IO.fromFuture(IO(client.storeBulkStudy(sources))).map(expect.same(_, ())) - - test("store bulk team endpoint"): client => - val sources = List( - lila.search.spec.TeamSourceWithId( - "id1", - lila.search.spec.TeamSource("names1", "desc1", 100) - ), - lila.search.spec.TeamSourceWithId( - "id2", - lila.search.spec.TeamSource("names2", "desc2", 200) - ) - ) - IO.fromFuture(IO(client.storeBulkTeam(sources))).map(expect.same(_, ())) - def testAppConfig = AppConfig( server = HttpServerConfig(ip"0.0.0.0", port"9999", false, shutdownTimeout = 1, false), elastic = ElasticConfig("http://0.0.0.0:9200") diff --git a/modules/app/src/test/scala/ElasticSearchContainer.scala b/modules/e2e/src/test/scala/ElasticSearchContainer.scala similarity index 100% rename from modules/app/src/test/scala/ElasticSearchContainer.scala rename to modules/e2e/src/test/scala/ElasticSearchContainer.scala diff --git a/modules/app/src/test/scala/IntegrationSuite.scala b/modules/e2e/src/test/scala/IntegrationSuite.scala similarity index 60% rename from modules/app/src/test/scala/IntegrationSuite.scala rename to modules/e2e/src/test/scala/IntegrationSuite.scala index 6e01d347..5957f328 100644 --- a/modules/app/src/test/scala/IntegrationSuite.scala +++ b/modules/e2e/src/test/scala/IntegrationSuite.scala @@ -5,6 +5,7 @@ package test import cats.effect.{ IO, Resource } import cats.syntax.all.* import com.comcast.ip4s.* +import lila.search.ingestor.given import lila.search.spec.* import org.http4s.Uri import org.typelevel.log4cats.noop.{ NoOpFactory, NoOpLogger } @@ -51,76 +52,70 @@ object IntegrationSuite extends IOSuite: _.healthCheck() .map(expect.same(_, HealthCheckOutput(ElasticStatus.green))) - test("forum"): _ => + test("forum"): res => Clients .search(uri) .use: service => for - _ <- service.mapping(Index.Forum) - _ <- service - .store( - "forum_id", - Source.forum( - ForumSource( - body = "a forum post", - topic = "chess", - topicId = "chess", - troll = false, - date = Instant.now().toEpochMilli(), - author = "nt9".some - ) - ) + _ <- res.esClient.putMapping(Index.Forum) + _ <- res.esClient.store( + Index.Forum, + Id("forum_id"), + ingestor.ForumSource( + body = "a forum post", + topic = "chess", + topicId = "chess", + troll = false, + date = Instant.now().toEpochMilli(), + author = "nt9".some ) - _ <- service.refresh(Index.Forum) + ) + _ <- res.esClient.refreshIndex(Index.Forum) x <- service.search(Query.forum("chess", false), from, size) y <- service.search(Query.forum("nt9", false), from, size) yield expect(x.hitIds.size == 1 && x == y) - test("team"): _ => + test("team"): res => Clients .search(uri) .use: service => for - _ <- service.mapping(Index.Team) - _ <- service - .store( - "team_id", - Source.team( - TeamSource( - name = "team name", - description = "team description", - 100 - ) - ) + _ <- res.esClient.putMapping(Index.Team) + _ <- res.esClient.store( + Index.Team, + Id("team_id"), + ingestor.TeamSource( + name = "team name", + description = "team description", + 100 ) - _ <- service.refresh(Index.Team) + ) + _ <- res.esClient.refreshIndex(Index.Team) x <- service.search(Query.team("team name"), from, size) y <- service.search(Query.team("team description"), from, size) yield expect(x.hitIds.size == 1 && x == y) - test("study"): _ => + test("study"): res => Clients .search(uri) .use: service => for - _ <- service.mapping(Index.Study) - _ <- service - .store( - "study_id", - Source.study( - StudySource( - name = "study name", - owner = "study owner", - members = List("member1", "member2"), - chapterNames = "names", - chapterTexts = "texts", - likes = 100, - public = true, - topics = List("topic1", "topic2") - ) - ) + _ <- res.esClient.putMapping(Index.Study) + _ <- res.esClient.store( + Index.Study, + Id("study_id"), + ingestor.StudySource( + name = "study name", + owner = "study owner", + members = List("member1", "member2"), + chapterNames = "names", + chapterTexts = "texts", + likes = 100, + public = true, + topics = List("topic1", "topic2") ) - _ <- service.refresh(Index.Study) + ) + _ <- res.esClient.refreshIndex(Index.Study) a <- service.search(Query.study("name"), from, size) b <- service.search(Query.study("study description"), from, size) c <- service.search(Query.study("topic1"), from, size) @@ -136,38 +131,36 @@ object IntegrationSuite extends IOSuite: duration = defaultIntRange, sorting = Sorting("field", "asc") ) - test("game"): _ => + test("game"): res => Clients .search(uri) .use: service => for - _ <- service.mapping(Index.Game) - _ <- service - .store( - "game_id", - Source.game( - GameSource( - status = 1, - turns = 100, - rated = true, - perf = 1, - winnerColor = 1, - date = SearchDateTime.fromInstant(Timestamp(1999, 10, 20, 12, 20, 20).toInstant), - analysed = false, - uids = List("uid1", "uid2").some, - winner = "uid1".some, - loser = "uid2".some, - averageRating = 150.some, - ai = none, - duration = 100.some, - clockInit = 100.some, - clockInc = 200.some, - whiteUser = "white".some, - blackUser = "black".some - ) - ) + _ <- res.esClient.putMapping(Index.Game) + _ <- res.esClient.store( + Index.Game, + Id("game_id"), + ingestor.GameSource( + status = 1, + turns = 100, + rated = true, + perf = 1, + winnerColor = 1, + date = SearchDateTime.fromInstant(Timestamp(1999, 10, 20, 12, 20, 20).toInstant), + analysed = false, + uids = List("uid1", "uid2").some, + winner = "uid1".some, + loser = "uid2".some, + averageRating = 150.some, + ai = none, + duration = 100.some, + clockInit = 100.some, + clockInc = 200.some, + whiteUser = "white".some, + blackUser = "black".some ) - _ <- service.refresh(Index.Game) + ) + _ <- res.esClient.refreshIndex(Index.Game) a <- service.search(defaultGame.copy(perf = List(1)), from, size) b <- service.search(defaultGame.copy(loser = "uid2".some), from, size) c <- service.search(defaultGame, from, size) diff --git a/modules/elastic/src/main/scala/study.scala b/modules/elastic/src/main/scala/study.scala index 2f7b5049..585c7a8c 100644 --- a/modules/elastic/src/main/scala/study.scala +++ b/modules/elastic/src/main/scala/study.scala @@ -25,9 +25,10 @@ case class Study(text: String, userId: Option[String]): val matcher: Query = if parsed.terms.isEmpty then matchAllQuery() else - multiMatchQuery( - parsed.terms.mkString(" ") - ).fields(Study.searchableFields*).analyzer("english").matchType("most_fields") + multiMatchQuery(parsed.terms.mkString(" ")) + .fields(Study.searchableFields*) + .analyzer("english") + .matchType("most_fields") boolQuery() .must: matcher :: List( diff --git a/modules/ingestor/src/main/scala/ingestor.forum.scala b/modules/ingestor/src/main/scala/ingestor.forum.scala index 978f7e1a..1d5c73b8 100644 --- a/modules/ingestor/src/main/scala/ingestor.forum.scala +++ b/modules/ingestor/src/main/scala/ingestor.forum.scala @@ -5,7 +5,6 @@ import cats.effect.IO import cats.syntax.all.* import com.mongodb.client.model.changestream.FullDocument import com.mongodb.client.model.changestream.OperationType.* -import lila.search.spec.ForumSource import mongo4cats.bson.Document import mongo4cats.database.MongoDatabase import mongo4cats.models.collection.ChangeStreamDocument diff --git a/modules/ingestor/src/main/scala/ingestor.game.scala b/modules/ingestor/src/main/scala/ingestor.game.scala index 15d5026b..2fb30e15 100644 --- a/modules/ingestor/src/main/scala/ingestor.game.scala +++ b/modules/ingestor/src/main/scala/ingestor.game.scala @@ -8,7 +8,6 @@ import chess.variant.* import com.mongodb.client.model.changestream.FullDocument import com.mongodb.client.model.changestream.OperationType.* import io.circe.* -import lila.search.spec.GameSource import mongo4cats.circe.* import mongo4cats.collection.MongoCollection import mongo4cats.database.MongoDatabase diff --git a/modules/ingestor/src/main/scala/ingestor.study.scala b/modules/ingestor/src/main/scala/ingestor.study.scala index 1ed8b89c..5ed29808 100644 --- a/modules/ingestor/src/main/scala/ingestor.study.scala +++ b/modules/ingestor/src/main/scala/ingestor.study.scala @@ -3,7 +3,6 @@ package ingestor import cats.effect.IO import cats.syntax.all.* -import lila.search.spec.StudySource import mongo4cats.bson.Document import mongo4cats.database.MongoDatabase import mongo4cats.operations.{ Filter, Projection } diff --git a/modules/ingestor/src/main/scala/ingestor.team.scala b/modules/ingestor/src/main/scala/ingestor.team.scala index 2240e891..2f8acc8f 100644 --- a/modules/ingestor/src/main/scala/ingestor.team.scala +++ b/modules/ingestor/src/main/scala/ingestor.team.scala @@ -5,7 +5,6 @@ import cats.effect.IO import cats.syntax.all.* import com.mongodb.client.model.changestream.FullDocument import com.mongodb.client.model.changestream.OperationType.* -import lila.search.spec.TeamSource import mongo4cats.bson.Document import mongo4cats.database.MongoDatabase import mongo4cats.models.collection.ChangeStreamDocument diff --git a/modules/ingestor/src/main/scala/package.scala b/modules/ingestor/src/main/scala/package.scala index 44a86afb..ba2b4287 100644 --- a/modules/ingestor/src/main/scala/package.scala +++ b/modules/ingestor/src/main/scala/package.scala @@ -5,7 +5,6 @@ import cats.effect.IO import cats.syntax.all.* import com.github.plokhotnyuk.jsoniter_scala.core.* import com.sksamuel.elastic4s.Indexable -import lila.search.spec.Source import mongo4cats.bson.Document import mongo4cats.collection.GenericMongoCollection import mongo4cats.models.collection.ChangeStreamDocument @@ -32,12 +31,6 @@ extension (doc: Document) doc.getString(_id) given [A: Schema]: Indexable[A] = (a: A) => writeToString(a) -given Indexable[Source] = - _ match - case f: Source.ForumCase => writeToString(f.forum) - case g: Source.GameCase => writeToString(g.game) - case s: Source.StudyCase => writeToString(s.study) - case t: Source.TeamCase => writeToString(t.team) extension (instant: Instant) inline def asBsonTimestamp: BsonTimestamp = BsonTimestamp(instant.getEpochSecond.toInt, 1) diff --git a/modules/ingestor/src/main/smithy/model.smithy b/modules/ingestor/src/main/smithy/model.smithy new file mode 100644 index 00000000..26ae628f --- /dev/null +++ b/modules/ingestor/src/main/smithy/model.smithy @@ -0,0 +1,113 @@ +$version: "2" + +namespace lila.search.ingestor + +use smithy.api#default +use smithy.api#jsonName + +use lila.search.core#Ids +use lila.search.core#FromInt +use lila.search.core#SizeInt +use lila.search.core#PlayerIds +use lila.search.core#Strings +use lila.search.core#IndexString +use lila.search.core#DateTime + +structure ForumSource { + @required + @jsonName("bo") + body: String + @required + @jsonName("to") + topic: String + @required + @jsonName("ti") + topicId: String + @jsonName("au") + author: String + @required + @jsonName("tr") + troll: Boolean + /// time in milliseconds + @required + @jsonName("da") + date: Long +} + +structure GameSource { + @required + @jsonName("s") + status: Integer + @required + @jsonName("t") + turns: Integer + @required + @jsonName("r") + rated: Boolean + @required + @jsonName("p") + perf: Integer + @jsonName("u") + uids: PlayerIds + @jsonName("w") + winner: String + @jsonName("o") + loser: String + @required + @jsonName("c") + winnerColor: Integer + @jsonName("a") + averageRating: Integer + @jsonName("i") + ai: Integer + @required + @jsonName("d") + date: DateTime + @jsonName("l") + duration: Integer + @jsonName("ct") + clockInit: Integer + @jsonName("ci") + clockInc: Integer + @required + @jsonName("n") + analysed: Boolean + @jsonName("wu") + whiteUser: String + @jsonName("bu") + blackUser: String + @jsonName("so") + source: Integer +} + +structure StudySource { + @required + name: String + @required + owner: String + @required + members: PlayerIds + @required + chapterNames: String + + @required + chapterTexts: String + @default + topics: Strings + @required + likes: Integer + @required + public: Boolean +} + +structure TeamSource { + @required + @jsonName("na") + name: String + @required + @jsonName("de") + description: String + @required + @jsonName("nbm") + nbMembers: Integer +}