216 lines
7.7 KiB
Kotlin
Executable File
216 lines
7.7 KiB
Kotlin
Executable File
package exh.search
|
|
|
|
import exh.metadata.sql.tables.SearchMetadataTable
|
|
import exh.metadata.sql.tables.SearchTagTable
|
|
import exh.metadata.sql.tables.SearchTitleTable
|
|
import java.util.Locale
|
|
|
|
class SearchEngine {
|
|
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
|
|
|
|
fun textToSubQueries(
|
|
namespace: String?,
|
|
component: Text?
|
|
): Pair<String, List<String>>? {
|
|
val maybeLenientComponent = component?.let {
|
|
if (!it.exact) {
|
|
it.asLenientTagQueries()
|
|
} else {
|
|
listOf(it.asQuery())
|
|
}
|
|
}
|
|
val componentTagQuery = maybeLenientComponent?.let {
|
|
val params = mutableListOf<String>()
|
|
it.joinToString(separator = " OR ", prefix = "(", postfix = ")") { q ->
|
|
params += q
|
|
"${SearchTagTable.TABLE}.${SearchTagTable.COL_NAME} LIKE ?"
|
|
} to params
|
|
}
|
|
return when {
|
|
namespace != null -> {
|
|
var query =
|
|
"""
|
|
(SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
|
WHERE ${SearchTagTable.COL_NAMESPACE} IS NOT NULL
|
|
AND ${SearchTagTable.COL_NAMESPACE} LIKE ?
|
|
""".trimIndent()
|
|
val params = mutableListOf(escapeLike(namespace))
|
|
if (componentTagQuery != null) {
|
|
query += "\n AND ${componentTagQuery.first}"
|
|
params += componentTagQuery.second
|
|
}
|
|
|
|
"$query)" to params
|
|
}
|
|
component != null -> {
|
|
// Match title + tags
|
|
val tagQuery =
|
|
"""
|
|
SELECT ${SearchTagTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTagTable.TABLE}
|
|
WHERE ${componentTagQuery!!.first}
|
|
""".trimIndent() to componentTagQuery.second
|
|
|
|
val titleQuery =
|
|
"""
|
|
SELECT ${SearchTitleTable.COL_MANGA_ID} AS $COL_MANGA_ID FROM ${SearchTitleTable.TABLE}
|
|
WHERE ${SearchTitleTable.COL_TITLE} LIKE ?
|
|
""".trimIndent() to listOf(component.asLenientTitleQuery())
|
|
|
|
"(${tagQuery.first} UNION ${titleQuery.first})".trimIndent() to
|
|
(tagQuery.second + titleQuery.second)
|
|
}
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
fun queryToSql(q: List<QueryComponent>): Pair<String, List<String>> {
|
|
val wheres = mutableListOf<String>()
|
|
val whereParams = mutableListOf<String>()
|
|
|
|
val include = mutableListOf<Pair<String, List<String>>>()
|
|
val exclude = mutableListOf<Pair<String, List<String>>>()
|
|
|
|
q.forEach { component ->
|
|
val query = if (component is Text) {
|
|
textToSubQueries(null, component)
|
|
} else if (component is Namespace) {
|
|
if (component.namespace == "uploader") {
|
|
wheres += "meta.${SearchMetadataTable.COL_UPLOADER} LIKE ?"
|
|
whereParams += component.tag!!.rawTextEscapedForLike()
|
|
null
|
|
} else {
|
|
if (component.tag!!.components.size > 0) {
|
|
// Match namespace + tags
|
|
textToSubQueries(component.namespace, component.tag)
|
|
} else {
|
|
// Perform namespace search
|
|
textToSubQueries(component.namespace, null)
|
|
}
|
|
}
|
|
} else error("Unknown query component!")
|
|
|
|
if (query != null) {
|
|
(if (component.excluded) exclude else include) += query
|
|
}
|
|
}
|
|
|
|
val completeParams = mutableListOf<String>()
|
|
var baseQuery =
|
|
"""
|
|
SELECT ${SearchMetadataTable.COL_MANGA_ID}
|
|
FROM ${SearchMetadataTable.TABLE} meta
|
|
""".trimIndent()
|
|
|
|
include.forEachIndexed { index, pair ->
|
|
baseQuery += "\n" + (
|
|
"""
|
|
INNER JOIN ${pair.first} i$index
|
|
ON i$index.$COL_MANGA_ID = meta.${SearchMetadataTable.COL_MANGA_ID}
|
|
""".trimIndent()
|
|
)
|
|
completeParams += pair.second
|
|
}
|
|
|
|
exclude.forEach {
|
|
wheres += """
|
|
(meta.${SearchMetadataTable.COL_MANGA_ID} NOT IN ${it.first})
|
|
""".trimIndent()
|
|
whereParams += it.second
|
|
}
|
|
if (wheres.isNotEmpty()) {
|
|
completeParams += whereParams
|
|
baseQuery += "\nWHERE\n"
|
|
baseQuery += wheres.joinToString("\nAND\n")
|
|
}
|
|
baseQuery += "\nORDER BY ${SearchMetadataTable.COL_MANGA_ID}"
|
|
|
|
return baseQuery to completeParams
|
|
}
|
|
|
|
fun parseQuery(query: String, enableWildcard: Boolean = true) = queryCache.getOrPut(query) {
|
|
val res = mutableListOf<QueryComponent>()
|
|
|
|
var inQuotes = false
|
|
val queuedRawText = StringBuilder()
|
|
val queuedText = mutableListOf<TextComponent>()
|
|
var namespace: Namespace? = null
|
|
|
|
var nextIsExcluded = false
|
|
var nextIsExact = false
|
|
|
|
fun flushText() {
|
|
if (queuedRawText.isNotEmpty()) {
|
|
queuedText += StringTextComponent(queuedRawText.toString())
|
|
queuedRawText.setLength(0)
|
|
}
|
|
}
|
|
|
|
fun flushToText() = Text().apply {
|
|
components += queuedText
|
|
queuedText.clear()
|
|
}
|
|
|
|
fun flushAll() {
|
|
flushText()
|
|
if (queuedText.isNotEmpty() || namespace != null) {
|
|
val component = namespace?.apply {
|
|
tag = flushToText()
|
|
namespace = null
|
|
} ?: flushToText()
|
|
component.excluded = nextIsExcluded
|
|
component.exact = nextIsExact
|
|
res += component
|
|
}
|
|
}
|
|
|
|
query.lowercase(Locale.getDefault()).forEach { char ->
|
|
if (char == '"') {
|
|
inQuotes = !inQuotes
|
|
} else if (enableWildcard && (char == '?' || char == '_')) {
|
|
flushText()
|
|
queuedText.add(SingleWildcard(char.toString()))
|
|
} else if (enableWildcard && (char == '*' || char == '%')) {
|
|
flushText()
|
|
queuedText.add(MultiWildcard(char.toString()))
|
|
} else if (char == '-' && !inQuotes && (queuedRawText.isBlank() || queuedRawText.last() == ' ')) {
|
|
nextIsExcluded = true
|
|
} else if (char == '$') {
|
|
nextIsExact = true
|
|
} else if (char == ':') {
|
|
flushText()
|
|
var flushed = flushToText().rawTextOnly()
|
|
// Map tag aliases
|
|
flushed = when (flushed) {
|
|
"a" -> "artist"
|
|
"c", "char" -> "character"
|
|
"f" -> "female"
|
|
"g", "creator", "circle" -> "group"
|
|
"l", "lang" -> "language"
|
|
"m" -> "male"
|
|
"p", "series" -> "parody"
|
|
"r" -> "reclass"
|
|
else -> flushed
|
|
}
|
|
namespace = Namespace(flushed, null)
|
|
} else if (char == ' ' && !inQuotes) {
|
|
flushAll()
|
|
} else {
|
|
queuedRawText.append(char)
|
|
}
|
|
}
|
|
flushAll()
|
|
|
|
res
|
|
}
|
|
|
|
companion object {
|
|
private const val COL_MANGA_ID = "cmid"
|
|
|
|
fun escapeLike(string: String): String {
|
|
return string.replace("\\", "\\\\")
|
|
.replace("_", "\\_")
|
|
.replace("%", "\\%")
|
|
}
|
|
}
|
|
}
|