fix(api): accent insensitive search

This commit is contained in:
Gauthier Roebroeck 2021-07-26 10:09:06 +08:00
parent 44bd09ac0b
commit 30c349afaf
8 changed files with 55 additions and 16 deletions

View file

@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.datasource package org.gotson.komga.infrastructure.datasource
import mu.KotlinLogging import mu.KotlinLogging
import org.gotson.komga.infrastructure.language.stripAccents
import org.springframework.jdbc.datasource.SimpleDriverDataSource import org.springframework.jdbc.datasource.SimpleDriverDataSource
import org.sqlite.Function import org.sqlite.Function
import org.sqlite.SQLiteConnection import org.sqlite.SQLiteConnection
@ -10,11 +11,20 @@ private val log = KotlinLogging.logger {}
class SqliteUdfDataSource : SimpleDriverDataSource() { class SqliteUdfDataSource : SimpleDriverDataSource() {
companion object {
const val udfStripAccents = "UDF_UNIDECODE"
}
override fun getConnection(): Connection = 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 = 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) { private fun createUdfRegexp(connection: SQLiteConnection) {
log.debug { "Adding custom REGEXP function" } 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())
}
}
)
}
} }

View file

@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.BookSearchWithReadProgress import org.gotson.komga.domain.model.BookSearchWithReadProgress
import org.gotson.komga.domain.model.ReadStatus 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.infrastructure.web.toFilePath
import org.gotson.komga.interfaces.rest.dto.AuthorDto import org.gotson.komga.interfaces.rest.dto.AuthorDto
import org.gotson.komga.interfaces.rest.dto.BookDto import org.gotson.komga.interfaces.rest.dto.BookDto
@ -56,6 +57,7 @@ class BookDtoDao(
"media.comment" to lower(m.COMMENT), "media.comment" to lower(m.COMMENT),
"media.mediaType" to lower(m.MEDIA_TYPE), "media.mediaType" to lower(m.MEDIA_TYPE),
"metadata.numberSort" to d.NUMBER_SORT, "metadata.numberSort" to d.NUMBER_SORT,
"metadata.title" to lower(d.TITLE),
"metadata.releaseDate" to d.RELEASE_DATE, "metadata.releaseDate" to d.RELEASE_DATE,
"readProgress.lastModified" to r.LAST_MODIFIED_DATE, "readProgress.lastModified" to r.LAST_MODIFIED_DATE,
"readList.number" to rlb.NUMBER "readList.number" to rlb.NUMBER
@ -266,7 +268,7 @@ class BookDtoDao(
if (!libraryIds.isNullOrEmpty()) c = c.and(b.LIBRARY_ID.`in`(libraryIds)) if (!libraryIds.isNullOrEmpty()) c = c.and(b.LIBRARY_ID.`in`(libraryIds))
if (!seriesIds.isNullOrEmpty()) c = c.and(b.SERIES_ID.`in`(seriesIds)) 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 (!mediaStatus.isNullOrEmpty()) c = c.and(m.STATUS.`in`(mediaStatus))
if (deleted == true) c = c.and(b.DELETED_DATE.isNotNull) if (deleted == true) c = c.and(b.DELETED_DATE.isNotNull)
if (deleted == false) c = c.and(b.DELETED_DATE.isNull) if (deleted == false) c = c.and(b.DELETED_DATE.isNull)

View file

@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.ReadList import org.gotson.komga.domain.model.ReadList
import org.gotson.komga.domain.persistence.ReadListRepository 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
import org.gotson.komga.jooq.tables.records.ReadlistRecord import org.gotson.komga.jooq.tables.records.ReadlistRecord
import org.jooq.DSLContext import org.jooq.DSLContext
@ -46,7 +47,7 @@ class ReadListDao(
.firstOrNull() .firstOrNull()
override fun searchAll(search: String?, pageable: Pageable): Page<ReadList> { 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() ?: DSL.trueCondition()
val count = dsl.selectCount() val count = dsl.selectCount()
@ -77,7 +78,7 @@ class ReadListDao(
.leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID)) .leftJoin(rlb).on(rl.ID.eq(rlb.READLIST_ID))
.leftJoin(b).on(rlb.BOOK_ID.eq(b.ID)) .leftJoin(b).on(rlb.BOOK_ID.eq(b.ID))
.where(b.LIBRARY_ID.`in`(belongsToLibraryIds)) .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) .fetch(0, String::class.java)
val count = ids.size val count = ids.size
@ -87,7 +88,7 @@ class ReadListDao(
val items = selectBase() val items = selectBase()
.where(rl.ID.`in`(ids)) .where(rl.ID.`in`(ids))
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } } .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) .orderBy(orderBy)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } .apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchAndMap(filterOnLibraryIds) .fetchAndMap(filterOnLibraryIds)

View file

@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.Author import org.gotson.komga.domain.model.Author
import org.gotson.komga.domain.persistence.ReferentialRepository 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
import org.gotson.komga.jooq.tables.records.BookMetadataAggregationAuthorRecord import org.gotson.komga.jooq.tables.records.BookMetadataAggregationAuthorRecord
import org.gotson.komga.jooq.tables.records.BookMetadataAuthorRecord import org.gotson.komga.jooq.tables.records.BookMetadataAuthorRecord
@ -36,7 +37,7 @@ class ReferentialDao(
dsl.selectDistinct(a.NAME, a.ROLE) dsl.selectDistinct(a.NAME, a.ROLE)
.from(a) .from(a)
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } } .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)) } } .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.orderBy(lower(a.NAME), a.ROLE) .orderBy(lower(a.NAME), a.ROLE)
.fetchInto(a) .fetchInto(a)
@ -46,7 +47,7 @@ class ReferentialDao(
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE) dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
.from(bmaa) .from(bmaa)
.leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) .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)) .and(s.LIBRARY_ID.eq(libraryId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(lower(bmaa.NAME), bmaa.ROLE) .orderBy(lower(bmaa.NAME), bmaa.ROLE)
@ -58,7 +59,7 @@ class ReferentialDao(
.from(bmaa) .from(bmaa)
.leftJoin(cs).on(bmaa.SERIES_ID.eq(cs.SERIES_ID)) .leftJoin(cs).on(bmaa.SERIES_ID.eq(cs.SERIES_ID))
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.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)) .and(cs.COLLECTION_ID.eq(collectionId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(lower(bmaa.NAME), bmaa.ROLE) .orderBy(lower(bmaa.NAME), bmaa.ROLE)
@ -69,7 +70,7 @@ class ReferentialDao(
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE) dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
.from(bmaa) .from(bmaa)
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.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(bmaa.SERIES_ID.eq(seriesId)) .and(bmaa.SERIES_ID.eq(seriesId))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.orderBy(lower(bmaa.NAME), bmaa.ROLE) .orderBy(lower(bmaa.NAME), bmaa.ROLE)
@ -108,7 +109,7 @@ class ReferentialDao(
.from(bmaa) .from(bmaa)
.apply { if (filterOnLibraryIds != null || filterBy?.type == FilterByType.LIBRARY) leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } .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)) } .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 { role?.let { and(bmaa.ROLE.eq(role)) } }
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } .apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
.apply { .apply {
@ -142,7 +143,7 @@ class ReferentialDao(
dsl.selectDistinct(a.NAME) dsl.selectDistinct(a.NAME)
.from(a) .from(a)
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } } .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)) } } .apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
.orderBy(a.NAME) .orderBy(a.NAME)
.fetch(a.NAME) .fetch(a.NAME)

View file

@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.SeriesCollection import org.gotson.komga.domain.model.SeriesCollection
import org.gotson.komga.domain.persistence.SeriesCollectionRepository 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
import org.gotson.komga.jooq.tables.records.CollectionRecord import org.gotson.komga.jooq.tables.records.CollectionRecord
import org.jooq.DSLContext import org.jooq.DSLContext
@ -45,7 +46,7 @@ class SeriesCollectionDao(
.firstOrNull() .firstOrNull()
override fun searchAll(search: String?, pageable: Pageable): Page<SeriesCollection> { 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() ?: DSL.trueCondition()
val count = dsl.selectCount() val count = dsl.selectCount()
@ -76,7 +77,7 @@ class SeriesCollectionDao(
.leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID)) .leftJoin(cs).on(c.ID.eq(cs.COLLECTION_ID))
.leftJoin(s).on(cs.SERIES_ID.eq(s.ID)) .leftJoin(s).on(cs.SERIES_ID.eq(s.ID))
.where(s.LIBRARY_ID.`in`(belongsToLibraryIds)) .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) .fetch(0, String::class.java)
val count = ids.size val count = ids.size
@ -86,7 +87,7 @@ class SeriesCollectionDao(
val items = selectBase() val items = selectBase()
.where(c.ID.`in`(ids)) .where(c.ID.`in`(ids))
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } } .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) .orderBy(orderBy)
.apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) } .apply { if (pageable.isPaged) limit(pageable.pageSize).offset(pageable.offset) }
.fetchAndMap(filterOnLibraryIds) .fetchAndMap(filterOnLibraryIds)

View file

@ -3,6 +3,7 @@ package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.domain.model.ReadStatus import org.gotson.komga.domain.model.ReadStatus
import org.gotson.komga.domain.model.SeriesSearch import org.gotson.komga.domain.model.SeriesSearch
import org.gotson.komga.domain.model.SeriesSearchWithReadProgress 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.infrastructure.web.toFilePath
import org.gotson.komga.interfaces.rest.dto.AuthorDto import org.gotson.komga.interfaces.rest.dto.AuthorDto
import org.gotson.komga.interfaces.rest.dto.BookMetadataAggregationDto 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 (!libraryIds.isNullOrEmpty()) c = c.and(s.LIBRARY_ID.`in`(libraryIds))
if (!collectionIds.isNullOrEmpty()) c = c.and(cs.COLLECTION_ID.`in`(collectionIds)) 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)) } searchRegex?.let { c = c.and((it.second.toColumn()).likeRegex(it.first)) }
if (!metadataStatus.isNullOrEmpty()) c = c.and(d.STATUS.`in`(metadataStatus)) if (!metadataStatus.isNullOrEmpty()) c = c.and(d.STATUS.`in`(metadataStatus))
if (!publishers.isNullOrEmpty()) c = c.and(lower(d.PUBLISHER).`in`(publishers.map { it.lowercase() })) if (!publishers.isNullOrEmpty()) c = c.and(lower(d.PUBLISHER).`in`(publishers.map { it.lowercase() }))

View file

@ -1,7 +1,10 @@
package org.gotson.komga.infrastructure.jooq package org.gotson.komga.infrastructure.jooq
import org.gotson.komga.infrastructure.datasource.SqliteUdfDataSource
import org.jooq.Field import org.jooq.Field
import org.jooq.SortField import org.jooq.SortField
import org.jooq.TableField
import org.jooq.impl.DSL
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId 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 = fun LocalDateTime.toCurrentTimeZone(): LocalDateTime =
this.atZone(ZoneId.of("Z")).withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime() this.atZone(ZoneId.of("Z")).withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime()
fun TableField<*, String>.udfStripAccents() =
DSL.function(SqliteUdfDataSource.udfStripAccents, String::class.java, this)

View file

@ -1,5 +1,6 @@
package org.gotson.komga.infrastructure.language package org.gotson.komga.infrastructure.language
import org.apache.commons.lang3.StringUtils
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalUnit 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) = fun LocalDateTime.notEquals(other: LocalDateTime, precision: TemporalUnit = ChronoUnit.MILLIS) =
this.truncatedTo(precision) != other.truncatedTo(precision) this.truncatedTo(precision) != other.truncatedTo(precision)
fun String.stripAccents(): String = StringUtils.stripAccents(this)