import com.google.common.base.Ticker
import com.google.common.cache.Cache
import com.google.common.cache.CacheBuilder
+import freenet.support.Logger
import net.pterodactylus.sone.data.Post
import net.pterodactylus.sone.data.PostReply
import net.pterodactylus.sone.data.Sone
import net.pterodactylus.sone.utils.Pagination
import net.pterodactylus.sone.utils.emptyToNull
+import net.pterodactylus.sone.utils.memoize
import net.pterodactylus.sone.utils.paginate
import net.pterodactylus.sone.utils.parameters
import net.pterodactylus.sone.web.WebInterface
import net.pterodactylus.util.text.StringEscaper
import net.pterodactylus.util.text.TextException
import java.util.concurrent.TimeUnit.MINUTES
+import javax.inject.Inject
/**
* This page lets the user search for posts and replies that contain certain
* words.
*/
-class SearchPage @JvmOverloads constructor(template: Template, webInterface: WebInterface, ticker: Ticker = Ticker.systemTicker()):
- SoneTemplatePage("search.html", template, "Page.Search.Title", webInterface, false) {
+class SearchPage(template: Template, webInterface: WebInterface, ticker: Ticker = Ticker.systemTicker()) :
+ SoneTemplatePage("search.html", webInterface, template, "Page.Search.Title") {
+
+ @Inject constructor(template: Template, webInterface: WebInterface) :
+ this(template, webInterface, Ticker.systemTicker())
private val cache: Cache<Iterable<Phrase>, Pagination<Post>> = CacheBuilder.newBuilder().ticker(ticker).expireAfterAccess(5, MINUTES).build()
override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) {
+ val startTime = System.currentTimeMillis()
val phrases = try {
freenetRequest.parameters["query"].emptyToNull?.parse()
} catch (te: TextException) {
}
}
+ val soneNameCache = { sone: Sone -> sone.names() }.memoize()
val sonePagination = webInterface.core.sones
- .scoreAndPaginate(phrases) { it.allText() }
+ .scoreAndPaginate(phrases) { it.allText(soneNameCache) }
.apply { page = freenetRequest.parameters["sonePage"].emptyToNull?.toIntOrNull() ?: 0 }
val postPagination = cache.get(phrases) {
webInterface.core.sones
.flatMap(Sone::getPosts)
.filter { Post.FUTURE_POSTS_FILTER.apply(it) }
- .scoreAndPaginate(phrases) { it.allText() }
+ .scoreAndPaginate(phrases) { it.allText(soneNameCache) }
}.apply { page = freenetRequest.parameters["postPage"].emptyToNull?.toIntOrNull() ?: 0 }
+ Logger.normal(SearchPage::class.java, "Finished search for “${freenetRequest.parameters["query"]}” in ${System.currentTimeMillis() - startTime}ms.")
templateContext["sonePagination"] = sonePagination
templateContext["soneHits"] = sonePagination.items
templateContext["postPagination"] = postPagination
.paginate(webInterface.core.preferences.postsPerPage)
private fun Sone.names() =
- listOf(name, profile.firstName, profile.middleName, profile.lastName)
- .filterNotNull()
- .joinToString("")
+ with(profile) {
+ listOf(name, firstName, middleName, lastName)
+ .filterNotNull()
+ .joinToString("")
+ }
- private fun Sone.allText() =
- (names() + profile.fields.map { "${it.name} ${it.value}" }.joinToString(" ", " ")).toLowerCase()
+ private fun Sone.allText(soneNameCache: (Sone) -> String) =
+ (soneNameCache(this) + profile.fields.map { "${it.name} ${it.value}" }.joinToString(" ", " ")).toLowerCase()
- private fun Post.allText() =
- (text + recipient.orNull()?.let { " ${it.names()}" } + webInterface.core.getReplies(id)
+ private fun Post.allText(soneNameCache: (Sone) -> String) =
+ (text + recipient.orNull()?.let { " ${soneNameCache(it)}" } + webInterface.core.getReplies(id)
.filter { PostReply.FUTURE_REPLY_FILTER.apply(it) }
- .map { "${it.sone.names()} ${it.text}" }.joinToString(" ", " ")).toLowerCase()
+ .map { "${soneNameCache(it.sone)} ${it.text}" }.joinToString(" ", " ")).toLowerCase()
+
+ private fun Iterable<Phrase>.indicesFor(text: String, predicate: (Phrase) -> Boolean) =
+ filter(predicate).map(Phrase::phrase).map(String::toLowerCase).flatMap { text.findAll(it) }
private fun score(text: String, phrases: Iterable<Phrase>): Double {
val requiredPhrases = phrases.count { it.required }
- val requiredHits = phrases.filter(Phrase::required)
- .map(Phrase::phrase)
- .flatMap { text.findAll(it) }
+ val requiredHits = phrases.indicesFor(text, Phrase::required)
.map { Math.pow(1 - it / text.length.toDouble(), 2.0) }
.sum()
- val optionalHits = phrases.filter(Phrase::optional)
- .map(Phrase::phrase)
- .flatMap { text.findAll(it) }
+ val optionalHits = phrases.indicesFor(text, Phrase::optional)
.map { Math.pow(1 - it / text.length.toDouble(), 2.0) }
.sum()
- val forbiddenHits = phrases.filter(Phrase::forbidden)
- .map(Phrase::phrase)
- .map { text.findAll(it).size }
- .sum()
+ val forbiddenHits = phrases.indicesFor(text, Phrase::forbidden)
+ .count()
return requiredHits * 3 + optionalHits + (requiredHits - requiredPhrases) * 5 - (forbiddenHits * 2)
}
private fun String.parse() =
StringEscaper.parseLine(this)
- .map(String::toLowerCase)
.map {
when {
it == "+" || it == "-" -> Phrase(it, OPTIONAL)