Files
TachiyomiSY/app/src/main/java/exh/search/SearchEngine.kt
T
2017-03-07 22:02:16 -05:00

153 lines
5.2 KiB
Kotlin

package exh.search
import exh.metadata.models.SearchableGalleryMetadata
import exh.metadata.models.Tag
class SearchEngine {
private val queryCache = mutableMapOf<String, List<QueryComponent>>()
fun matches(metadata: SearchableGalleryMetadata, query: List<QueryComponent>): Boolean {
fun matchTagList(tags: Sequence<Tag>,
component: Text): Boolean {
//Match tags
val tagMatcher = if(!component.exact)
component.asLenientRegex()
else
component.asRegex()
//Match beginning of tag
if (tags.find {
tagMatcher.testExact(it.name)
} != null) {
if(component.excluded) return false
} else {
//No tag matched for this component
return false
}
return true
}
val cachedLowercaseTitle = metadata.title?.toLowerCase()
val cachedLowercaseAltTitles = metadata.altTitles.map(String::toLowerCase)
for(component in query) {
if(component is Text) {
//Match title
if (component.asRegex().test(cachedLowercaseTitle)
|| cachedLowercaseAltTitles.find { component.asRegex().test(it) } != null) {
continue
}
//Match tags
if(!matchTagList(metadata.tags.entries.asSequence().flatMap { it.value.asSequence() },
component)) return false
} else if(component is Namespace) {
if(component.namespace == "uploader") {
//Match uploader
if(!component.tag?.rawTextOnly().equals(metadata.uploader,
ignoreCase = true)) {
return false
}
} else {
if(component.tag!!.components.size > 0) {
//Match namespace
val ns = metadata.tags.entries.asSequence().filter {
it.key == component.namespace
}.flatMap { it.value.asSequence() }
//Match tags
if (!matchTagList(ns, component.tag!!))
return false
} else {
//Perform namespace search
val hasNs = metadata.tags.entries.find {
it.key == component.namespace
} != null
if(hasNs && component.excluded)
return false
else if(!hasNs && !component.excluded)
return false
}
}
}
}
return true
}
fun parseQuery(query: String) = 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
}
}
for(char in query.toLowerCase()) {
if(char == '"') {
inQuotes = !inQuotes
} else if(char == '?' || char == '_') {
flushText()
queuedText.add(SingleWildcard())
} else if(char == '*' || char == '%') {
flushText()
queuedText.add(MultiWildcard())
} else if(char == '-') {
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
})
}