mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 07:23:34 +01:00
fix(api): accent insensitive search
This commit is contained in:
parent
44bd09ac0b
commit
30c349afaf
8 changed files with 55 additions and 16 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package org.gotson.komga.infrastructure.datasource
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.springframework.jdbc.datasource.SimpleDriverDataSource
|
||||
import org.sqlite.Function
|
||||
import org.sqlite.SQLiteConnection
|
||||
|
|
@ -10,11 +11,20 @@ private val log = KotlinLogging.logger {}
|
|||
|
||||
class SqliteUdfDataSource : SimpleDriverDataSource() {
|
||||
|
||||
companion object {
|
||||
const val udfStripAccents = "UDF_UNIDECODE"
|
||||
}
|
||||
|
||||
override fun getConnection(): Connection =
|
||||
super.getConnection().also { createUdfRegexp(it as SQLiteConnection) }
|
||||
super.getConnection().also { addAllUdf(it as SQLiteConnection) }
|
||||
|
||||
override fun getConnection(username: String, password: String): Connection =
|
||||
super.getConnection(username, password).also { createUdfRegexp(it as SQLiteConnection) }
|
||||
super.getConnection(username, password).also { addAllUdf(it as SQLiteConnection) }
|
||||
|
||||
private fun addAllUdf(connection: SQLiteConnection) {
|
||||
createUdfRegexp(connection)
|
||||
createUdfStripAccents(connection)
|
||||
}
|
||||
|
||||
private fun createUdfRegexp(connection: SQLiteConnection) {
|
||||
log.debug { "Adding custom REGEXP function" }
|
||||
|
|
@ -30,4 +40,18 @@ class SqliteUdfDataSource : SimpleDriverDataSource() {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUdfStripAccents(connection: SQLiteConnection) {
|
||||
log.debug { "Adding custom $udfStripAccents function" }
|
||||
Function.create(
|
||||
connection, udfStripAccents,
|
||||
object : Function() {
|
||||
override fun xFunc() =
|
||||
when (val text = value_text(0)) {
|
||||
null -> error("Argument must not be null")
|
||||
else -> result(text.stripAccents())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
|
|||
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.gotson.komga.infrastructure.web.toFilePath
|
||||
import org.gotson.komga.interfaces.rest.dto.AuthorDto
|
||||
import org.gotson.komga.interfaces.rest.dto.BookDto
|
||||
|
|
@ -56,6 +57,7 @@ class BookDtoDao(
|
|||
"media.comment" to lower(m.COMMENT),
|
||||
"media.mediaType" to lower(m.MEDIA_TYPE),
|
||||
"metadata.numberSort" to d.NUMBER_SORT,
|
||||
"metadata.title" to lower(d.TITLE),
|
||||
"metadata.releaseDate" to d.RELEASE_DATE,
|
||||
"readProgress.lastModified" to r.LAST_MODIFIED_DATE,
|
||||
"readList.number" to rlb.NUMBER
|
||||
|
|
@ -266,7 +268,7 @@ class BookDtoDao(
|
|||
|
||||
if (!libraryIds.isNullOrEmpty()) c = c.and(b.LIBRARY_ID.`in`(libraryIds))
|
||||
if (!seriesIds.isNullOrEmpty()) c = c.and(b.SERIES_ID.`in`(seriesIds))
|
||||
searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) }
|
||||
searchTerm?.let { c = c.and(d.TITLE.udfStripAccents().containsIgnoreCase(it.stripAccents())) }
|
||||
if (!mediaStatus.isNullOrEmpty()) c = c.and(m.STATUS.`in`(mediaStatus))
|
||||
if (deleted == true) c = c.and(b.DELETED_DATE.isNotNull)
|
||||
if (deleted == false) c = c.and(b.DELETED_DATE.isNull)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
|
|||
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.gotson.komga.jooq.Tables
|
||||
import org.gotson.komga.jooq.tables.records.ReadlistRecord
|
||||
import org.jooq.DSLContext
|
||||
|
|
@ -46,7 +47,7 @@ class ReadListDao(
|
|||
.firstOrNull()
|
||||
|
||||
override fun searchAll(search: String?, pageable: Pageable): Page<ReadList> {
|
||||
val conditions = search?.let { rl.NAME.containsIgnoreCase(it) }
|
||||
val conditions = search?.let { rl.NAME.udfStripAccents().containsIgnoreCase(it.stripAccents()) }
|
||||
?: DSL.trueCondition()
|
||||
|
||||
val count = dsl.selectCount()
|
||||
|
|
@ -77,7 +78,7 @@ class ReadListDao(
|
|||
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
|
||||
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
|
||||
.where(b.LIBRARY_ID.`in`(belongsToLibraryIds))
|
||||
.apply { search?.let { and(rl.NAME.containsIgnoreCase(it)) } }
|
||||
.apply { search?.let { and(rl.NAME.udfStripAccents().containsIgnoreCase(it.stripAccents())) } }
|
||||
.fetch(0, String::class.java)
|
||||
|
||||
val count = ids.size
|
||||
|
|
@ -87,7 +88,7 @@ class ReadListDao(
|
|||
val items = selectBase()
|
||||
.where(rl.ID.`in`(ids))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { search?.let { and(rl.NAME.containsIgnoreCase(it)) } }
|
||||
.apply { search?.let { and(rl.NAME.udfStripAccents().containsIgnoreCase(it.stripAccents())) } }
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
|
|||
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.persistence.ReferentialRepository
|
||||
import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.gotson.komga.jooq.Tables
|
||||
import org.gotson.komga.jooq.tables.records.BookMetadataAggregationAuthorRecord
|
||||
import org.gotson.komga.jooq.tables.records.BookMetadataAuthorRecord
|
||||
|
|
@ -36,7 +37,7 @@ class ReferentialDao(
|
|||
dsl.selectDistinct(a.NAME, a.ROLE)
|
||||
.from(a)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } }
|
||||
.where(a.NAME.containsIgnoreCase(search))
|
||||
.where(a.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents()))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(a.NAME), a.ROLE)
|
||||
.fetchInto(a)
|
||||
|
|
@ -46,7 +47,7 @@ class ReferentialDao(
|
|||
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
|
||||
.from(bmaa)
|
||||
.leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID))
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents()))
|
||||
.and(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(bmaa.NAME), bmaa.ROLE)
|
||||
|
|
@ -58,7 +59,7 @@ class ReferentialDao(
|
|||
.from(bmaa)
|
||||
.leftJoin(cs).on(bmaa.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } }
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents()))
|
||||
.and(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(bmaa.NAME), bmaa.ROLE)
|
||||
|
|
@ -69,7 +70,7 @@ class ReferentialDao(
|
|||
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
|
||||
.from(bmaa)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } }
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents()))
|
||||
.and(bmaa.SERIES_ID.eq(seriesId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(bmaa.NAME), bmaa.ROLE)
|
||||
|
|
@ -108,7 +109,7 @@ class ReferentialDao(
|
|||
.from(bmaa)
|
||||
.apply { if (filterOnLibraryIds != null || filterBy?.type == FilterByType.LIBRARY) leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) }
|
||||
.apply { if (filterBy?.type == FilterByType.COLLECTION) leftJoin(cs).on(bmaa.SERIES_ID.eq(cs.SERIES_ID)) }
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.where(bmaa.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents()))
|
||||
.apply { role?.let { and(bmaa.ROLE.eq(role)) } }
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.apply {
|
||||
|
|
@ -142,7 +143,7 @@ class ReferentialDao(
|
|||
dsl.selectDistinct(a.NAME)
|
||||
.from(a)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } }
|
||||
.where(a.NAME.containsIgnoreCase(search))
|
||||
.where(a.NAME.udfStripAccents().containsIgnoreCase(search.stripAccents()))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(a.NAME)
|
||||
.fetch(a.NAME)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
|
|||
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
|
||||
import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.gotson.komga.jooq.Tables
|
||||
import org.gotson.komga.jooq.tables.records.CollectionRecord
|
||||
import org.jooq.DSLContext
|
||||
|
|
@ -45,7 +46,7 @@ class SeriesCollectionDao(
|
|||
.firstOrNull()
|
||||
|
||||
override fun searchAll(search: String?, pageable: Pageable): Page<SeriesCollection> {
|
||||
val conditions = search?.let { c.NAME.containsIgnoreCase(it) }
|
||||
val conditions = search?.let { c.NAME.udfStripAccents().containsIgnoreCase(it.stripAccents()) }
|
||||
?: DSL.trueCondition()
|
||||
|
||||
val count = dsl.selectCount()
|
||||
|
|
@ -76,7 +77,7 @@ class SeriesCollectionDao(
|
|||
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
|
||||
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.`in`(belongsToLibraryIds))
|
||||
.apply { search?.let { and(c.NAME.containsIgnoreCase(it)) } }
|
||||
.apply { search?.let { and(c.NAME.udfStripAccents().containsIgnoreCase(it.stripAccents())) } }
|
||||
.fetch(0, String::class.java)
|
||||
|
||||
val count = ids.size
|
||||
|
|
@ -86,7 +87,7 @@ class SeriesCollectionDao(
|
|||
val items = selectBase()
|
||||
.where(c.ID.`in`(ids))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.apply { search?.let { and(c.NAME.containsIgnoreCase(it)) } }
|
||||
.apply { search?.let { and(c.NAME.udfStripAccents().containsIgnoreCase(it.stripAccents())) } }
|
||||
.orderBy(orderBy)
|
||||
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
|
||||
.fetchAndMap(filterOnLibraryIds)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package org.gotson.komga.infrastructure.jooq
|
|||
import org.gotson.komga.domain.model.ReadStatus
|
||||
import org.gotson.komga.domain.model.SeriesSearch
|
||||
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress
|
||||
import org.gotson.komga.infrastructure.language.stripAccents
|
||||
import org.gotson.komga.infrastructure.web.toFilePath
|
||||
import org.gotson.komga.interfaces.rest.dto.AuthorDto
|
||||
import org.gotson.komga.interfaces.rest.dto.BookMetadataAggregationDto
|
||||
|
|
@ -229,7 +230,7 @@ class SeriesDtoDao(
|
|||
|
||||
if (!libraryIds.isNullOrEmpty()) c = c.and(s.LIBRARY_ID.`in`(libraryIds))
|
||||
if (!collectionIds.isNullOrEmpty()) c = c.and(cs.COLLECTION_ID.`in`(collectionIds))
|
||||
searchTerm?.let { c = c.and(d.TITLE.containsIgnoreCase(it)) }
|
||||
searchTerm?.let { c = c.and(d.TITLE.udfStripAccents().containsIgnoreCase(it.stripAccents())) }
|
||||
searchRegex?.let { c = c.and((it.second.toColumn()).likeRegex(it.first)) }
|
||||
if (!metadataStatus.isNullOrEmpty()) c = c.and(d.STATUS.`in`(metadataStatus))
|
||||
if (!publishers.isNullOrEmpty()) c = c.and(lower(d.PUBLISHER).`in`(publishers.map { it.lowercase() }))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
|
||||
import org.jooq.Field
|
||||
import org.jooq.SortField
|
||||
import org.jooq.TableField
|
||||
import org.jooq.impl.DSL
|
||||
import org.springframework.data.domain.Sort
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
|
@ -18,3 +21,6 @@ fun Sort.toOrderBy(sorts: Map<String, Field<out Any>>): List<SortField<out Any>>
|
|||
|
||||
fun LocalDateTime.toCurrentTimeZone(): LocalDateTime =
|
||||
this.atZone(ZoneId.of("Z")).withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime()
|
||||
|
||||
fun TableField<*, String>.udfStripAccents() =
|
||||
DSL.function(SqliteUdfDataSource.udfStripAccents, String::class.java, this)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.gotson.komga.infrastructure.language
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.temporal.TemporalUnit
|
||||
|
|
@ -36,3 +37,5 @@ fun <T, R : Any> Iterable<T>.mostFrequent(transform: (T) -> R?): R? {
|
|||
|
||||
fun LocalDateTime.notEquals(other: LocalDateTime, precision: TemporalUnit = ChronoUnit.MILLIS) =
|
||||
this.truncatedTo(precision) != other.truncatedTo(precision)
|
||||
|
||||
fun String.stripAccents(): String = StringUtils.stripAccents(this)
|
||||
|
|
|
|||
Loading…
Reference in a new issue