Skip to content

Commit

Permalink
feat: add full text search
Browse files Browse the repository at this point in the history
  • Loading branch information
bayang committed Jul 15, 2023
1 parent ea93e12 commit ec9f714
Show file tree
Hide file tree
Showing 19 changed files with 905 additions and 158 deletions.
11 changes: 10 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ dependencies {
implementation("org.springdoc:springdoc-openapi-security:$springdocVersion")
implementation("org.springdoc:springdoc-openapi-kotlin:$springdocVersion")
implementation("org.springdoc:springdoc-openapi-data-rest:$springdocVersion")

val luceneVersion = "9.7.0"
implementation("org.apache.lucene:lucene-core:$luceneVersion")
implementation("org.apache.lucene:lucene-analysis-common:$luceneVersion")
implementation("org.apache.lucene:lucene-queryparser:$luceneVersion")
implementation("org.apache.lucene:lucene-backward-codecs:$luceneVersion")
}

tasks.withType<Test> {
Expand All @@ -93,7 +99,10 @@ tasks.withType<Test> {

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
freeCompilerArgs = listOf(
"-Xjsr305=strict",
"-opt-in=kotlin.time.ExperimentalTime"
)
jvmTarget = "11"
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/jelu-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/jelu-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const search = () => {
console.log(searchQuery.value)
if (StringUtils.isNotBlank(searchQuery.value)) {
showAdvanced.value = false
router.push({ path: '/search', query: { title: searchQuery.value } })
router.push({ path: '/search', query: { q: searchQuery.value } })
}
}
Expand Down
130 changes: 5 additions & 125 deletions src/jelu-ui/src/components/SearchResultsDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,7 @@ const { t } = useI18n({
useTitle('Jelu | Search')
const titleQuery: Ref<string|undefined> = useRouteQuery('title', undefined)
const isbn10Query: Ref<string|undefined> = useRouteQuery('isbn10', undefined)
const isbn13Query: Ref<string|undefined> = useRouteQuery('isbn13', undefined)
const seriesQuery: Ref<string|undefined> = useRouteQuery('series', undefined)
const authorsQuery: Ref<Array<string>> = useRouteQuery('authors', [])
const translatorsQuery: Ref<Array<string>> = useRouteQuery('translators', [])
const tagsQuery: Ref<Array<string>> = useRouteQuery('tags', [])
const tagsArrayString: Ref<string> = ref("")
if (tagsQuery.value.length > 0) {
tagsArrayString.value = tagsQuery.value.join(",")
}
const authorsArrayString: Ref<string> = ref("")
if (authorsQuery.value.length > 0) {
authorsArrayString.value = authorsQuery.value.join(",")
}
const translatorsArrayString: Ref<string> = ref("")
if (translatorsQuery.value.length > 0) {
translatorsArrayString.value = translatorsQuery.value.join(",")
}
const searchQuery: Ref<string|undefined> = useRouteQuery('q', undefined)
const books: Ref<Array<Book>> = ref([]);
Expand All @@ -61,10 +40,7 @@ const search = () => {
progress.value = true
updatePageLoading(true)
dataService.findBooks(
titleQuery.value,
isbn10Query.value, isbn13Query.value,
seriesQuery.value, authorsQuery.value,
translatorsQuery.value,tagsQuery.value,
searchQuery.value,
pageAsNumber.value - 1, perPage.value,
sortQuery.value, libraryFilter.value
)
Expand All @@ -88,10 +64,6 @@ const search = () => {
})
}
const arrayParam = (input: string) => {
return input.split(",")
}
watch([page, sortQuery, libraryFilter], (newVal, oldVal) => {
console.log(page.value)
console.log(newVal + " " + oldVal)
Expand All @@ -100,32 +72,14 @@ watch([page, sortQuery, libraryFilter], (newVal, oldVal) => {
}
})
watch(authorsArrayString, (newVal, oldVal) => {
authorsQuery.value = arrayParam(authorsArrayString.value)
})
watch(translatorsArrayString, (newVal, oldVal) => {
translatorsQuery.value = arrayParam(translatorsArrayString.value)
})
watch(tagsArrayString, (newVal, oldVal) => {
tagsQuery.value = arrayParam(tagsArrayString.value)
})
const convertedBooks = computed(() => books.value?.map(b => ObjectUtils.toUserBook(b)))
function modalClosed() {
console.log("modal closed")
search()
}
if (titleQuery.value != null ||
isbn10Query.value != null ||
isbn13Query.value != null ||
seriesQuery.value != null ||
authorsQuery.value.length > 0 ||
translatorsQuery.value.length > 0 ||
tagsQuery.value.length > 0) {
if (searchQuery.value != null) {
search()
}
Expand Down Expand Up @@ -246,8 +200,8 @@ if (titleQuery.value != null ||
<div class="basis-full">
<o-field class="title-input">
<o-input
v-model="titleQuery"
:placeholder="t('labels.search_title')"
v-model="searchQuery"
:placeholder="t('labels.search_query')"
type="search"
icon="magnify"
icon-clickable
Expand All @@ -264,82 +218,8 @@ if (titleQuery.value != null ||
/>
</o-field>
</div>
<div class="flex flex-wrap justify-center justify-items-center search-form-extended">
<div class="search-form-extended-input">
<o-input
v-model="isbn10Query"
:placeholder="t('book.isbn10')"
type="search"
icon="magnify"
icon-clickable
icon-pack="mdi"
class="input focus:input-accent"
@keyup.enter="search"
/>
</div>
<div class="search-form-extended-input">
<o-input
v-model="isbn13Query"
:placeholder="t('book.isbn13')"
type="search"
icon="magnify"
icon-clickable
icon-pack="mdi"
class="input focus:input-accent"
@keyup.enter="search"
/>
</div>
<div class="search-form-extended-input">
<o-input
v-model="authorsArrayString"
:placeholder="t('book.author', 2)"
type="search"
icon="magnify"
icon-clickable
icon-pack="mdi"
class="input focus:input-accent"
@keyup.enter="search"
/>
</div>
<div class="search-form-extended-input">
<o-input
v-model="translatorsArrayString"
:placeholder="t('book.translator', 2)"
type="search"
icon="magnify"
icon-clickable
icon-pack="mdi"
class="input focus:input-accent"
@keyup.enter="search"
/>
</div>
<div class="search-form-extended-input">
<o-input
v-model="tagsArrayString"
:placeholder="t('book.tag', 2)"
type="search"
icon="magnify"
icon-clickable
icon-pack="mdi"
class="input focus:input-accent"
@keyup.enter="search"
/>
</div>
<div class="search-form-extended-input">
<o-input
v-model="seriesQuery"
:placeholder="t('book.series')"
type="search"
icon="magnify"
icon-clickable
icon-pack="mdi"
class="input focus:input-accent"
@keyup.enter="search"
/>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-1 my-4">
<div
v-for="book in convertedBooks"
Expand Down
6 changes: 2 additions & 4 deletions src/jelu-ui/src/components/SeriesBooks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@ watch([() => series.value, page, sortQuery], (newVal, oldVal) => {
const getBooks = () => {
getBooksIsLoading.value = true
dataService.findBooks(undefined,
undefined, undefined,
series.value, undefined,
undefined, undefined,
dataService.findBooks(
`series:"${series.value}"`,
pageAsNumber.value - 1, perPage.value, sortQuery.value, LibraryFilter.ONLY_USER_BOOKS)
.then(res => {
console.log(res)
Expand Down
3 changes: 2 additions & 1 deletion src/jelu-ui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
"add" : "Add",
"add_to_my_books" : "Not yet in your books, click to add it",
"add_translator": "Add a translator",
"reading_list_from_name" : "Reading list from {name}"
"reading_list_from_name" : "Reading list from {name}",
"search_query" : "Enter search query..."
},
"settings" : {
"pick_language" : "Pick your language",
Expand Down
35 changes: 32 additions & 3 deletions src/jelu-ui/src/services/DataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ class DataService {
}
}

findBooks = async (title?: string, isbn10?: string, isbn13?: string,
findBooksDetailed = async (title?: string, isbn10?: string, isbn13?: string,
series?: string, authors?: Array<string>, translators?: Array<string>,
tags?: Array<string>, page?: number, size?: number, sort?: string,
libraryFilter?: LibraryFilter) => {
Expand Down Expand Up @@ -725,6 +725,35 @@ class DataService {
}
}

findBooks = async (query?: string, page?: number, size?: number, sort?: string,
libraryFilter?: LibraryFilter) => {
try {
const response = await this.apiClient.get<Page<Book>>(`${this.API_BOOK}`, {
params: {
q: query,
page: page,
size: size,
sort: sort,
libraryFilter: libraryFilter
},
paramsSerializer: {
serialize : (params) => {
return qs.stringify(params, { arrayFormat: 'comma' })
}},
});
console.log("called find books")
console.log(response)
return response.data;
}
catch (error) {
if (axios.isAxiosError(error) && error.response) {
console.log("error axios " + error.response.status + " " + error.response.data.error)
}
console.log("error find books " + (error as AxiosError).code)
throw new Error("error find books " + error)
}
}

deleteUserBook = async (userbookId: string) => {
try {
const response = await this.apiClient.delete(`${this.API_USERBOOK}/${userbookId}`);
Expand Down Expand Up @@ -1429,15 +1458,15 @@ class DataService {
checkIsbnExists = async (isbn10: string|undefined, isbn13: string|undefined) => {
console.log(isbn10 + " " + isbn13)
if (StringUtils.isNotBlank(isbn10)) {
const res = await this.findBooks(undefined, isbn10, undefined)
const res = await this.findBooks(`isbn:${isbn10}`)
console.log(res.empty)
if (!res.empty) {
return res.content[0]
}
}
if (StringUtils.isNotBlank(isbn13)) {
console.log(isbn13)
const res = await this.findBooks(undefined, undefined, isbn13)
const res = await this.findBooks(`isbn:${isbn13}`)
console.log(res.empty)
if (!res.empty) {
return res.content[0]
Expand Down
19 changes: 18 additions & 1 deletion src/main/kotlin/io/github/bayang/jelu/config/JeluProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ data class JeluProperties(
Ldap(),
Proxy()
),
val metadataProviders: List<MetaDataProvider>?
val metadataProviders: List<MetaDataProvider>?,
val lucene: Lucene = Lucene(indexAnalyzer = IndexAnalyzer())
) {

data class MetaDataProvider(
Expand Down Expand Up @@ -76,4 +77,20 @@ data class JeluProperties(
val adminName: String = "",
val header: String = "X-Authenticated-User"
)

data class IndexAnalyzer(
@get:Positive
var minGram: Int = 3,
@get:Positive
var maxGram: Int = 10,
var preserveOriginal: Boolean = true
)

data class Lucene(
@get:NotBlank
var dataDirectory: String = "",

var indexAnalyzer: IndexAnalyzer

)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.bayang.jelu.config

import io.github.bayang.jelu.search.MultiLingualAnalyzer
import io.github.bayang.jelu.search.MultiLingualNGramAnalyzer
import org.apache.lucene.store.ByteBuffersDirectory
import org.apache.lucene.store.Directory
import org.apache.lucene.store.FSDirectory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import java.nio.file.Paths

@Configuration
class LuceneConfiguration(
private val jeluProperties: JeluProperties
) {

@Bean
fun indexAnalyzer() =
with(jeluProperties.lucene.indexAnalyzer) {
MultiLingualNGramAnalyzer(minGram, maxGram, preserveOriginal)
}

@Bean
fun searchAnalyzer() =
MultiLingualAnalyzer()

@Bean
@Profile("test")
fun memoryDirectory(): Directory =
ByteBuffersDirectory()

@Bean
@Profile("!test")
fun diskDirectory(): Directory =
FSDirectory.open(Paths.get(jeluProperties.lucene.dataDirectory))
}
Loading

0 comments on commit ec9f714

Please sign in to comment.