Skip to content

Commit

Permalink
feature: Re-write page content extraction for search feature
Browse files Browse the repository at this point in the history
  • Loading branch information
LichtHund committed Jul 6, 2024
1 parent cecceae commit db242e4
Show file tree
Hide file tree
Showing 23 changed files with 593 additions and 211 deletions.
4 changes: 2 additions & 2 deletions backend/src/main/kotlin/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import dev.triumphteam.backend.api.database.DocVersions
import dev.triumphteam.backend.api.database.Pages
import dev.triumphteam.backend.api.database.Projects
import io.ktor.server.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
Expand Down Expand Up @@ -42,5 +42,5 @@ public fun main() {
)
}

embeddedServer(Netty, module = Application::module, port = 8001, watchPaths = listOf("classes")).start(true)
embeddedServer(CIO, module = Application::module, port = 8001, watchPaths = listOf("classes")).start(true)
}
5 changes: 5 additions & 0 deletions backend/src/main/kotlin/Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.triumphteam.backend

import dev.triumphteam.backend.api.apiRoutes
import dev.triumphteam.backend.api.auth.TriumphPrincipal
import dev.triumphteam.backend.meilisearch.Meili
import dev.triumphteam.backend.website.websiteRoutes
import dev.triumphteam.website.JsonSerializer
import io.ktor.http.CacheControl
Expand Down Expand Up @@ -68,6 +69,10 @@ public fun Application.module() {
install(ForwardedHeaders)
install(XForwardedHeaders)

install(Meili) {

}

routing {

staticResources("/static", "static")
Expand Down
15 changes: 8 additions & 7 deletions backend/src/main/kotlin/api/ProjectSetup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ public fun setupRepository(projects: File) {
it.mkdirs()
}

val banner = page.banner
val description = page.description

bannerMaker.create(
icon = projectIcon,
group = banner.group,
title = banner.title,
subTitle = banner.subTitle,
group = description.group,
title = description.title,
subTitle = description.subTitle,
output = pageDir.resolve("banner.png"),
)

Expand All @@ -87,9 +87,10 @@ public fun setupRepository(projects: File) {
this.project = projectEntity
this.version = versionEntity
this.content = page.content
this.title = banner.title ?: ""
this.subTitle = banner.subTitle ?: ""
this.summary = page.summary
this.path = page.path
this.title = description.title ?: ""
this.subTitle = description.subTitle ?: ""
this.summary = description.summary
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions backend/src/main/kotlin/api/database/Entities.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.triumphteam.backend.api.database

import dev.triumphteam.website.project.Navigation
import dev.triumphteam.website.project.PageSummary
import dev.triumphteam.website.project.Page
import org.jetbrains.exposed.dao.Entity
import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.IntEntity
Expand Down Expand Up @@ -40,10 +40,11 @@ public object Pages : IntIdTable("pages") {
reference("project_id", Projects, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
public val version: Column<EntityID<Int>> =
reference("version_id", DocVersions, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
public val path: Column<String> = text("path")
public val content: Column<String> = text("content")
public val title: Column<String> = text("title")
public val subTitle: Column<String> = text("sub_title")
public val summary: Column<PageSummary> = serializable<PageSummary>("summary")
public val summary: Column<List<Page.Summary>> = serializable<List<Page.Summary>>("summary")

init {
uniqueIndex("page_project_version_uq", pageId, project, version)
Expand Down Expand Up @@ -74,8 +75,9 @@ public class PageEntity(id: EntityID<Int>) : IntEntity(id) {
public var pageId: String by Pages.pageId
public var project: ProjectEntity by ProjectEntity referencedOn Pages.project
public var version: DocVersionEntity by DocVersionEntity referencedOn Pages.version
public var path: String by Pages.path
public var content: String by Pages.content
public var title: String by Pages.title
public var subTitle: String by Pages.subTitle
public var summary: PageSummary by Pages.summary
public var summary: List<Page.Summary> by Pages.summary
}
48 changes: 48 additions & 0 deletions backend/src/main/kotlin/meilisearch/ApiAuthProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.triumphteam.backend.meilisearch

import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.AuthProvider
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.headers
import io.ktor.http.HttpHeaders
import io.ktor.http.auth.AuthScheme
import io.ktor.http.auth.HttpAuthHeader

public fun Auth.api(
token: String,
sendWithoutRequestCallback: (HttpRequestBuilder) -> Boolean = { true },
realm: String? = null,
) {
providers.add(ApiAuthProvider(token, sendWithoutRequestCallback, realm))
}

public class ApiAuthProvider(
private val token: String,
private val sendWithoutRequestCallback: (HttpRequestBuilder) -> Boolean = { true },
private val realm: String?,
) : AuthProvider {

@Suppress("OverridingDeprecatedMember")
@Deprecated("Please use sendWithoutRequest function instead", ReplaceWith("sendWithoutRequest"))
override val sendWithoutRequest: Boolean
get() = error("Deprecated")

override fun sendWithoutRequest(request: HttpRequestBuilder): Boolean = sendWithoutRequestCallback(request)

/** Checks if current provider is applicable to the request. */
override fun isApplicable(auth: HttpAuthHeader): Boolean {
if (auth.authScheme != AuthScheme.Bearer) return false
if (realm == null) return true
if (auth !is HttpAuthHeader.Parameterized) return false

return auth.parameter("realm") == realm
}

/** Adds an authentication method headers and credentials. */
override suspend fun addRequestHeaders(request: HttpRequestBuilder, authHeader: HttpAuthHeader?) {
request.headers {
if (contains(HttpHeaders.Authorization)) remove(HttpHeaders.Authorization)
append(HttpHeaders.Authorization, "Bearer $token")
}
}
}
189 changes: 189 additions & 0 deletions backend/src/main/kotlin/meilisearch/MeiliClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package dev.triumphteam.backend.meilisearch

import dev.triumphteam.backend.meilisearch.annotation.PrimaryKey
import dev.triumphteam.website.JsonSerializer
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.resources.Resources
import io.ktor.client.plugins.resources.delete
import io.ktor.client.plugins.resources.post
import io.ktor.client.request.parameter
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.URLProtocol
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.resources.Resource
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable

/** A client to handle connection between the application and https://www.meilisearch.com/. */
public class MeiliClient(
host: String,
port: Int,
private val apiKey: String = "",
protocol: URLProtocol,
) {

public val client: HttpClient = HttpClient(CIO) {
install(Resources)
install(Auth) { api(apiKey) } // Auto setup authentication
install(ContentNegotiation) { json(JsonSerializer.json) } // Using Kotlin serialization for content negotiation
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.BODY
}

defaultRequest {
this.host = host
this.port = port
url {
this.protocol = protocol
}
}
}

/** Gets an index, doesn't always mean the index exists within Meili. */
public fun index(
uid: String,
primaryKey: String? = null,
): Index = Index(uid, primaryKey)

/** Representation of an index. Contains the needed operations with the index. */
public inner class Index(
public val uid: String,
public val primaryKey: String?,
) {

/** Create the index. Success even if it already exists. */
public suspend fun create(): HttpResponse = client.post(Indexes()) {
contentType(ContentType.Application.Json)
setBody(Create(uid, primaryKey))
}.also {
println(it)
}

/** Deletes the current index. Success even if it doesn't exist. */
public suspend fun delete(): HttpResponse = client.delete(Indexes.Uid(uid = uid))

/** Search for specific content in the index. */
public suspend inline fun <reified T> search(query: String, filter: String?): List<T> {
return searchFull<T>(query, filter).hits
}

/** [search] but returns all the data ([SearchResult]) provided by the search. */
public suspend inline fun <reified T> searchFull(query: String, filter: String?): SearchResult<T> {
// TODO: Handle errors.
return client.post(Indexes.Uid.Search(Indexes.Uid(uid = uid))) {
contentType(ContentType.Application.Json)
setBody(SearchRequest(query, filter))
}.body()
}

/** Add documents to the index. Creates a new index if none exists. */
public suspend inline fun <reified T> addDocuments(
documents: List<T>,
primaryKey: String? = null,
): HttpResponse {
val pk = primaryKey ?: this.primaryKey ?: run {
T::class.java.constructors.firstOrNull()?.parameters?.find { it.isAnnotationPresent(PrimaryKey::class.java) }?.name
}

return client.post(Indexes.Uid.Documents(Indexes.Uid(uid = uid))) {
contentType(ContentType.Application.Json)
pk?.let { parameter(PRIMARY_KEY_PARAM, it) }
setBody<List<T>>(documents)
}.also {
println(it)
println(it.body<String>())
}
}

/** Transfers all the documents from this index into the passed [index], and deletes this. */
public suspend fun transferTo(index: Index): HttpResponse {
val createResponse = index.create()

// If there was an error creating the new index we return the response
if (!createResponse.status.isSuccess()) return createResponse

val swapResponse = client.post(Indexes.Swap()) {
setBody(listOf(Swap(listOf(uid, this@Index.uid))))
}

// If there was an error swapping the new indexes we return the response
if (!swapResponse.status.isSuccess()) return swapResponse

// Then we delete the current index
return delete()
}
}

/** Resource location for "$url/indexes". */
@Serializable
@Resource("/indexes")
public class Indexes {

/** Resource location for "$url/indexes/[uid]". */
@Serializable
@Resource("{uid}")
public class Uid(public val parent: Indexes = Indexes(), public val uid: String) {

/** Resource location for "$url/indexes/[uid]/search". */
@Serializable
@Resource("/search")
public class Search(public val parent: Uid)

/** Resource location for "$url/indexes/[uid]/documents". */
@Serializable
@Resource("/documents")
public class Documents(public val parent: Uid)
}

/** Resource location for "$url/indexes/swap-indexes". */
@Serializable
@Resource("/swap-indexes")
public class Swap(public val parent: Indexes = Indexes())
}

/** Serializable class for the create end point. */
@Serializable
public data class Create(
val uid: String,
val primaryKey: String?,
)

/** Serializable class for the search result from end point. */
@Serializable
public data class SearchResult<T>(
val hits: List<T>,
val query: String,
val processingTimeMs: Long,
val limit: Int,
val offset: Int,
val estimatedTotalHits: Int,
)

/** Serializable class for the search end point. */
@Serializable
public data class SearchRequest(
public val q: String,
public val filter: String?,
)

/** Serializable class for the swap end point. */
@Serializable
public data class Swap(public val indexes: List<String>)

public companion object {
public const val PRIMARY_KEY_PARAM: String = "primaryKey"
}
}
46 changes: 46 additions & 0 deletions backend/src/main/kotlin/meilisearch/MeiliPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.triumphteam.backend.meilisearch

import io.ktor.http.URLProtocol
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.BaseApplicationPlugin
import io.ktor.server.application.application
import io.ktor.server.application.plugin
import io.ktor.util.AttributeKey
import io.ktor.util.pipeline.PipelineContext
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable

public class Meili(config: Configuration) {

public val client: MeiliClient = config.createClient()

public class Configuration {
private var host: String = "0.0.0.0"
private var port: Int = 7700
private var apiKey: String = "masterKey"
private var protocol: URLProtocol = URLProtocol.HTTP

internal fun createClient() = MeiliClient(host, port, apiKey, protocol)
}

public companion object Plugin : BaseApplicationPlugin<Application, Configuration, Meili> {

override val key: AttributeKey<Meili> = AttributeKey("Meili")

override fun install(pipeline: Application, configure: Configuration.() -> Unit): Meili {
return Meili(Configuration().apply(configure))
}
}
}

public suspend inline fun <reified T> PipelineContext<*, ApplicationCall>.search(
index: String,
query: String,
filter: String? = null,
): List<T> = with(this.application.plugin(Meili).client) {
return index(index).search(query, filter)
}

public suspend inline fun PipelineContext<*, ApplicationCall>.index(index: String): MeiliClient.Index =
application.plugin(Meili).client.index(index)
5 changes: 5 additions & 0 deletions backend/src/main/kotlin/meilisearch/annotation/PrimaryKey.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.triumphteam.backend.meilisearch.annotation

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
public annotation class PrimaryKey
Loading

0 comments on commit db242e4

Please sign in to comment.