Implement search engine.
This commit is contained in:
parent
c836f52460
commit
02c70d868d
@ -200,6 +200,9 @@ dependencies {
|
|||||||
//SnappyDB (EH)
|
//SnappyDB (EH)
|
||||||
compile 'io.paperdb:paperdb:2.0'
|
compile 'io.paperdb:paperdb:2.0'
|
||||||
|
|
||||||
|
//JVE (Regex) (EH)
|
||||||
|
compile 'ru.lanwen.verbalregex:java-verbal-expressions:1.4'
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
//Paper DB screws up tests
|
//Paper DB screws up tests
|
||||||
/*testCompile 'junit:junit:4.12'
|
/*testCompile 'junit:junit:4.12'
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
|
|||||||
import exh.metadata.MetadataHelper
|
import exh.metadata.MetadataHelper
|
||||||
import exh.metadata.copyTo
|
import exh.metadata.copyTo
|
||||||
import exh.metadata.models.ExGalleryMetadata
|
import exh.metadata.models.ExGalleryMetadata
|
||||||
|
import exh.search.SearchEngine
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
@ -24,6 +25,8 @@ class EHentaiMetadata(override val id: Int,
|
|||||||
|
|
||||||
val internalEx = EHentai(id - 2, exh, context)
|
val internalEx = EHentai(id - 2, exh, context)
|
||||||
|
|
||||||
|
val searchEngine = SearchEngine()
|
||||||
|
|
||||||
override val baseUrl: String
|
override val baseUrl: String
|
||||||
get() = throw UnsupportedOperationException()
|
get() = throw UnsupportedOperationException()
|
||||||
override val lang: String
|
override val lang: String
|
||||||
@ -105,8 +108,11 @@ class EHentaiMetadata(override val id: Int,
|
|||||||
|
|
||||||
override fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>)
|
override fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>)
|
||||||
= Observable.fromCallable {
|
= Observable.fromCallable {
|
||||||
|
val parsed = searchEngine.parseQuery(query)
|
||||||
page.mangas.addAll(sortedByTimeGalleries().filter { manga ->
|
page.mangas.addAll(sortedByTimeGalleries().filter { manga ->
|
||||||
filters.isEmpty() || filters.filter { it.id == manga.genre }.isNotEmpty()
|
filters.isEmpty() || filters.filter { it.id == manga.genre }.isNotEmpty()
|
||||||
|
}.filter {
|
||||||
|
searchEngine.matches(it, parsed)
|
||||||
}.mapToManga())
|
}.mapToManga())
|
||||||
page
|
page
|
||||||
}!!
|
}!!
|
||||||
|
5
app/src/main/java/exh/VerbelExpressionExtensions.kt
Normal file
5
app/src/main/java/exh/VerbelExpressionExtensions.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package exh
|
||||||
|
|
||||||
|
import ru.lanwen.verbalregex.VerbalExpression
|
||||||
|
|
||||||
|
fun VerbalExpression.Builder.anyChar() = add(".")!!
|
3
app/src/main/java/exh/search/MultiWildcard.kt
Normal file
3
app/src/main/java/exh/search/MultiWildcard.kt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
class MultiWildcard : TextComponent()
|
4
app/src/main/java/exh/search/Namespace.kt
Normal file
4
app/src/main/java/exh/search/Namespace.kt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
class Namespace(var namespace: Text,
|
||||||
|
var tag: Text? = null) : QueryComponent()
|
6
app/src/main/java/exh/search/QueryComponent.kt
Normal file
6
app/src/main/java/exh/search/QueryComponent.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
open class QueryComponent {
|
||||||
|
var excluded = false
|
||||||
|
var exact = false
|
||||||
|
}
|
124
app/src/main/java/exh/search/SearchEngine.kt
Normal file
124
app/src/main/java/exh/search/SearchEngine.kt
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
import exh.metadata.models.ExGalleryMetadata
|
||||||
|
import exh.metadata.models.Tag
|
||||||
|
import ru.lanwen.verbalregex.VerbalExpression
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SearchEngine {
|
||||||
|
//TODO Namespace alias
|
||||||
|
fun matches(metadata: ExGalleryMetadata, query: List<QueryComponent>): Boolean {
|
||||||
|
|
||||||
|
fun matchTagList(tags: List<Tag>,
|
||||||
|
component: Text,
|
||||||
|
builder: VerbalExpression.Builder,
|
||||||
|
built: VerbalExpression): Boolean {
|
||||||
|
//Match tags
|
||||||
|
val tagMatcher = if(!component.exact)
|
||||||
|
builder.anything().build()
|
||||||
|
else
|
||||||
|
built
|
||||||
|
//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
|
||||||
|
}
|
||||||
|
|
||||||
|
for(component in query) {
|
||||||
|
if(component is Text) {
|
||||||
|
val builder = component.asRegex()
|
||||||
|
val built = builder.build()
|
||||||
|
//Match title
|
||||||
|
if (built.test(metadata.title?.toLowerCase())
|
||||||
|
|| built.test(metadata.altTitle?.toLowerCase())) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//Match tags
|
||||||
|
if(!matchTagList(metadata.tags.entries.flatMap(MutableMap.MutableEntry<String, ArrayList<Tag>>::value),
|
||||||
|
component,
|
||||||
|
builder,
|
||||||
|
built))
|
||||||
|
return false
|
||||||
|
} else if(component is Namespace) {
|
||||||
|
//Match namespace
|
||||||
|
val ns = metadata.tags.entries.filter {
|
||||||
|
it.key == component.namespace.rawTextOnly()
|
||||||
|
}.flatMap { it.value }
|
||||||
|
//Match tags
|
||||||
|
val builder = component.tag!!.asRegex()
|
||||||
|
val built = builder.build()
|
||||||
|
if(!matchTagList(ns, component.tag!!, builder, built))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseQuery(query: String): List<QueryComponent> {
|
||||||
|
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()) {
|
||||||
|
val component = namespace?.apply {
|
||||||
|
tag = flushToText()
|
||||||
|
} ?: 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()
|
||||||
|
namespace = Namespace(flushToText(), null)
|
||||||
|
} else if(char == ' ' && !inQuotes) {
|
||||||
|
flushAll()
|
||||||
|
} else {
|
||||||
|
queuedRawText.append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushAll()
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
3
app/src/main/java/exh/search/SingleWildcard.kt
Normal file
3
app/src/main/java/exh/search/SingleWildcard.kt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
class SingleWildcard : TextComponent()
|
3
app/src/main/java/exh/search/StringTextComponent.kt
Normal file
3
app/src/main/java/exh/search/StringTextComponent.kt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
class StringTextComponent(val value: String) : TextComponent()
|
24
app/src/main/java/exh/search/Text.kt
Normal file
24
app/src/main/java/exh/search/Text.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
import exh.anyChar
|
||||||
|
import ru.lanwen.verbalregex.VerbalExpression
|
||||||
|
|
||||||
|
class Text: QueryComponent() {
|
||||||
|
val components = mutableListOf<TextComponent>()
|
||||||
|
|
||||||
|
fun asRegex(): VerbalExpression.Builder {
|
||||||
|
val builder = VerbalExpression.regex()
|
||||||
|
for(component in components) {
|
||||||
|
when(component) {
|
||||||
|
is StringTextComponent -> builder.then(component.value)
|
||||||
|
is SingleWildcard -> builder.anyChar()
|
||||||
|
is MultiWildcard -> builder.zeroOrMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rawTextOnly() = components
|
||||||
|
.filter { it is StringTextComponent }
|
||||||
|
.joinToString(separator = "")
|
||||||
|
}
|
3
app/src/main/java/exh/search/TextComponent.kt
Normal file
3
app/src/main/java/exh/search/TextComponent.kt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package exh.search
|
||||||
|
|
||||||
|
open class TextComponent
|
Loading…
x
Reference in New Issue
Block a user