Compare commits

..

No commits in common. "c15a2aa9391e7b7880154cf93c89e9c2809ff822" and "f2afed09bede110e044d34e56d8ec0f6f00b1bcc" have entirely different histories.

10 changed files with 155 additions and 116 deletions

View File

@ -34,7 +34,7 @@ const { locale } = useI18n()
const settings = useSettingsStore() const settings = useSettingsStore()
const { unlocked } = useDevMode() const { unlocked } = useDevMode()
const toast = useToast() const toast = useToast()
const typesenseClient = useTypesenseClient() const { documentsApi } = useTypesenseClient()
// Restaurar estado desde URL antes de crear los refs // Restaurar estado desde URL antes de crear los refs
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState() const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
@ -106,17 +106,20 @@ async function applyBibleStudyFilter() {
isValidating.value = true isValidating.value = true
try { try {
const res = await typesenseClient.multiSearch.perform({ const res = await documentsApi.multiSearch({
searches: [{ multiSearchParameters: {},
collection: props.mainCollection, multiSearchSearchesParameter: {
q: '*', searches: [{
query_by: 'title', collection: props.mainCollection,
filter_by: `bible_study:=${val}`, q: '*',
per_page: 1, queryBy: 'title',
include_fields: 'bible_study,title', filterBy: `bible_study:=${val}`,
use_cache: true, perPage: 1,
cache_ttl: 3600 includeFields: 'bible_study,title',
}] useCache: true,
cacheTtl: 3600
}]
}
}) })
const hit = (res?.results?.[0] as { hits?: Array<{ document: { title?: string } }> })?.hits?.[0] const hit = (res?.results?.[0] as { hits?: Array<{ document: { title?: string } }> })?.hits?.[0]
@ -222,7 +225,7 @@ async function fetchDocumentWithParagraphs(docId: string) {
selectedDocument.value = null selectedDocument.value = null
selectedParagraphs.value = [] selectedParagraphs.value = []
try { try {
const detail = await fetchDocumentDetail(typesenseClient, props.mainCollection, props.paragraphsCollection, docId, controller.signal) const detail = await fetchDocumentDetail(documentsApi, props.mainCollection, props.paragraphsCollection, docId, controller.signal)
if (seq !== detailSeq) return if (seq !== detailSeq) return
if (detail) { if (detail) {
const rawParagraphs = detail.paragraphs as unknown as ParagraphDoc[] const rawParagraphs = detail.paragraphs as unknown as ParagraphDoc[]

View File

@ -41,7 +41,7 @@ export function useDocumentDetailFetch() {
} }
async function fetchDocumentDetail( async function fetchDocumentDetail(
typesenseClient: ReturnType<typeof useTypesenseClient>, documentsApi: ReturnType<typeof useTypesenseClient>['documentsApi'],
mainCollection: string, mainCollection: string,
paragraphsCollection: string, paragraphsCollection: string,
docId: string, docId: string,
@ -50,17 +50,20 @@ export function useDocumentDetailFetch() {
const cached = getCached(mainCollection, docId) const cached = getCached(mainCollection, docId)
if (cached) return cached if (cached) return cached
const res = await typesenseClient.multiSearch.perform({ const res = await documentsApi.multiSearch({
searches: [{ multiSearchParameters: {},
collection: mainCollection, multiSearchSearchesParameter: {
q: '*', searches: [{
query_by: 'title', collection: mainCollection,
filter_by: `id:=${docId} && $${paragraphsCollection}(id: *)`, q: '*',
include_fields: `*, $${paragraphsCollection}(*)`, queryBy: 'title',
use_cache: true, filterBy: `id:=${docId} && $${paragraphsCollection}(id: *)`,
cache_ttl: 3600 includeFields: `*, $${paragraphsCollection}(*)`,
}] useCache: true,
}, {}, { abortSignal: signal }) cacheTtl: 3600
}]
}
}, { signal })
const hit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0] const hit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0]
if (!hit) return null if (!hit) return null

View File

@ -6,7 +6,7 @@ interface BibleStudyChip {
const activeBibleStudies = useState<BibleStudyChip[]>('bible-study-chips', () => []) const activeBibleStudies = useState<BibleStudyChip[]>('bible-study-chips', () => [])
export function useFilters() { export function useFilters() {
const typesenseClient = useTypesenseClient() const { documentsApi } = useTypesenseApi()
const toast = useToast() const toast = useToast()
const bibleStudyInput = ref<number | null>(null) const bibleStudyInput = ref<number | null>(null)
@ -25,17 +25,20 @@ export function useFilters() {
isValidating.value = true isValidating.value = true
try { try {
const res = await typesenseClient.multiSearch.perform({ const res = await documentsApi.multiSearch({
searches: [{ multiSearchParameters: {},
collection: mainCollection, multiSearchSearchesParameter: {
q: '*', searches: [{
query_by: 'title', collection: mainCollection,
filter_by: `bible_study:=${val}`, q: '*',
per_page: 1, queryBy: 'title',
include_fields: 'bible_study,title', filterBy: `bible_study:=${val}`,
use_cache: true, perPage: 1,
cache_ttl: 3600 includeFields: 'bible_study,title',
}] useCache: true,
cacheTtl: 3600
}]
}
}) })
const hit = (res?.results?.[0] as { hits?: Array<{ document: { title?: string } }> })?.hits?.[0] const hit = (res?.results?.[0] as { hits?: Array<{ document: { title?: string } }> })?.hits?.[0]

View File

@ -49,7 +49,7 @@ export function usePublicationFetch() {
const detailParagraphs = ref<TypesenseParagraphHit[]>([]) const detailParagraphs = ref<TypesenseParagraphHit[]>([])
const detailParagraphsLoading = ref(false) const detailParagraphsLoading = ref(false)
const typesenseClient = useTypesenseClient() const { documentsApi } = useTypesenseClient()
const { fetchDocumentDetail } = useDocumentDetailFetch() const { fetchDocumentDetail } = useDocumentDetailFetch()
let fetchSeq = 0 let fetchSeq = 0
@ -76,7 +76,7 @@ export function usePublicationFetch() {
detailDocument.value = null detailDocument.value = null
detailParagraphs.value = [] detailParagraphs.value = []
try { try {
const detail = await fetchDocumentDetail(typesenseClient, config.main, config.paragraphs, docId, controller.signal) const detail = await fetchDocumentDetail(documentsApi, config.main, config.paragraphs, docId, controller.signal)
if (seq !== fetchSeq) return if (seq !== fetchSeq) return
if (detail) { if (detail) {
const rawParagraphs = detail.paragraphs as unknown as ParagraphDoc[] const rawParagraphs = detail.paragraphs as unknown as ParagraphDoc[]

View File

@ -1,6 +1,9 @@
import type Client from 'typesense/Typesense/Client' /**
* Devuelve la instancia única del cliente Typesense creada por el plugin
export function useTypesenseClient(): Client { * `typesense-client.client.ts`, en vez de construir un `Configuration` +
* 14 sub-APIs nuevos en cada llamada (lo que hace `useTypesenseApi()` crudo).
*/
export function useTypesenseClient() {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
return nuxtApp.$typesenseClient as Client return nuxtApp.$typesenseApi
} }

View File

@ -79,7 +79,7 @@ export interface FlatTypesenseSearchOptions {
} }
export function useFlatTypesenseSearch<TDoc = Record<string, unknown>>(options: FlatTypesenseSearchOptions) { export function useFlatTypesenseSearch<TDoc = Record<string, unknown>>(options: FlatTypesenseSearchOptions) {
const typesenseClient = useTypesenseClient() const { documentsApi } = useTypesenseClient()
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
const query = ref(options.initialQuery) const query = ref(options.initialQuery)
@ -116,21 +116,24 @@ export function useFlatTypesenseSearch<TDoc = Record<string, unknown>>(options:
const typePage = isInfinite ? (append ? currentPage.value + 1 : 1) : page const typePage = isInfinite ? (append ? currentPage.value + 1 : 1) : page
try { try {
const multi = await typesenseClient.multiSearch.perform({ const multi = await documentsApi.multiSearch({
searches: [{ multiSearchParameters: {},
collection: options.collection, multiSearchSearchesParameter: {
q: exactSearch.value && q ? `"${q}"` : q || '*', searches: [{
query_by: options.queryBy, collection: options.collection,
include_fields: options.includeFields ?? '*', q: exactSearch.value && q ? `"${q}"` : q || '*',
filter_by: options.filterBy(), queryBy: options.queryBy,
per_page: options.pageSize(), includeFields: options.includeFields ?? '*',
page: typePage, filterBy: options.filterBy(),
highlight_full_fields: options.queryBy, perPage: options.pageSize(),
highlight_fields: options.queryBy, page: typePage,
highlight_start_tag: '<mark class="search-match">', highlightFullFields: options.queryBy,
highlight_end_tag: '</mark>' highlightFields: options.queryBy,
}] highlightStartTag: '<mark class="search-match">',
}, {}, { abortSignal: signal }) highlightEndTag: '</mark>'
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return if (!runner.isCurrent(seq)) return
@ -212,13 +215,13 @@ export interface TypesenseGroupedParagraphHit {
} }
export interface TypesenseGroupedHit { export interface TypesenseGroupedHit {
group_key: string[] groupKey: string[]
hits: TypesenseGroupedParagraphHit[] hits: TypesenseGroupedParagraphHit[]
} }
interface GroupedSearchResponse { interface GroupedSearchResponse {
found: number found: number
grouped_hits?: TypesenseGroupedHit[] groupedHits?: TypesenseGroupedHit[]
hits?: Array<{ document: Record<string, unknown> }> hits?: Array<{ document: Record<string, unknown> }>
} }
@ -260,7 +263,7 @@ export interface GroupedTypesenseSearchOptions {
const META_FIELDS = 'id,title,date,timestamp,place,city,state,country,type,slug,draft' const META_FIELDS = 'id,title,date,timestamp,place,city,state,country,type,slug,draft'
export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions) { export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions) {
const typesenseClient = useTypesenseClient() const { documentsApi } = useTypesenseClient()
const docMetaCache = useDocMetaCache() const docMetaCache = useDocMetaCache()
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
const highlightAffixNumTokens = options.highlightAffixNumTokens ?? 15 const highlightAffixNumTokens = options.highlightAffixNumTokens ?? 15
@ -341,8 +344,8 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
function browseFilterBy() { function browseFilterBy() {
const base = options.browseFilterBy ? options.browseFilterBy() : options.filterBy() const base = options.browseFilterBy ? options.browseFilterBy() : options.filterBy()
return options.isUnlocked() return options.isUnlocked()
? `${base} && has_paragraphs:=true` ? base
: `${base} && private:=false && has_paragraphs:=true` : `${base} && private:=false`
} }
const runner = createAbortableRunner() const runner = createAbortableRunner()
@ -364,30 +367,33 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
try { try {
const shouldSortByDate = sortMode.value === 'date' && q.trim() const shouldSortByDate = sortMode.value === 'date' && q.trim()
const multi = await typesenseClient.multiSearch.perform({ const multi = await documentsApi.multiSearch({
searches: [{ multiSearchParameters: {},
collection: options.paragraphsCollection, multiSearchSearchesParameter: {
q: exactSearch.value && q ? `"${q}"` : q || '*', searches: [{
query_by: options.queryBy, collection: options.paragraphsCollection,
filter_by: searchFilterBy(), q: exactSearch.value && q ? `"${q}"` : q || '*',
...(shouldSortByDate ? { sort_by: `$${options.mainCollection}(timestamp:desc)` } : {}), queryBy: options.queryBy,
per_page: options.pageSize(), filterBy: searchFilterBy(),
page: typePage, ...(shouldSortByDate ? { sortBy: `$${options.mainCollection}(timestamp:desc)` } : {}),
highlight_full_fields: options.queryBy, perPage: options.pageSize(),
highlight_fields: options.queryBy, page: typePage,
highlight_start_tag: '<mark class="search-match">', highlightFullFields: options.queryBy,
highlight_end_tag: '</mark>', highlightFields: options.queryBy,
highlight_affix_num_tokens: highlightAffixNumTokens, highlightStartTag: '<mark class="search-match">',
group_by: options.groupByField, highlightEndTag: '</mark>',
include_fields: `*, $${options.mainCollection}(${META_FIELDS})` highlightAffixNumTokens,
}] groupBy: options.groupByField,
}, {}, { abortSignal: signal }) includeFields: `*, $${options.mainCollection}(${META_FIELDS})`
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return if (!runner.isCurrent(seq)) return
const res = (multi?.results?.[0] ?? {}) as GroupedSearchResponse const res = (multi?.results?.[0] ?? {}) as GroupedSearchResponse
const rawGroups = res?.grouped_hits ?? [] const rawGroups = res?.groupedHits ?? []
const newGroups: SearchGroup[] = rawGroups.map(g => ({ const newGroups: SearchGroup[] = rawGroups.map(g => ({
docId: g.group_key[0]!, docId: g.groupKey[0]!,
firstHit: g.hits[0]!, firstHit: g.hits[0]!,
allHits: g.hits allHits: g.hits
})) }))
@ -429,20 +435,22 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
const isInfinite = options.paginationType() === 'infinite_scroll' const isInfinite = options.paginationType() === 'infinite_scroll'
const typePage = isInfinite ? (append ? browsePage.value + 1 : 1) : page const typePage = isInfinite ? (append ? browsePage.value + 1 : 1) : page
console.log("testing" + browseFilterBy())
try { try {
const multi = await typesenseClient.multiSearch.perform({ const multi = await documentsApi.multiSearch({
searches: [{ multiSearchParameters: {},
collection: options.mainCollection, multiSearchSearchesParameter: {
q: '*', searches: [{
query_by: 'title', collection: options.mainCollection,
filter_by: browseFilterBy(), q: '*',
sort_by: 'timestamp:desc', queryBy: 'title',
per_page: options.pageSize(), filterBy: browseFilterBy(),
page: typePage, sortBy: 'timestamp:desc',
include_fields: META_FIELDS perPage: options.pageSize(),
}] page: typePage,
}, {}, { abortSignal: signal }) includeFields: META_FIELDS
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return if (!runner.isCurrent(seq)) return
const result = (multi?.results?.[0] as GroupedSearchResponse | undefined) const result = (multi?.results?.[0] as GroupedSearchResponse | undefined)
const rawHits = result?.hits ?? [] const rawHits = result?.hits ?? []

View File

@ -1,23 +1,23 @@
import Typesense from 'typesense' /**
* Construye el cliente de Typesense (Configuration + 14 sub-APIs) UNA sola
* vez por carga de página, en vez de reconstruirlo en cada componente que
* llama `useTypesenseApi()` (SearchPanel.vue, entrelineas.vue,
* usePublicationFetch.ts). Se expone vía `nuxtApp.$typesenseApi` (tipado
* automáticamente por Nuxt gracias al `provide` de retorno); el acceso lo
* da el composable `useTypesenseClient()`.
*
* Sin sufijo `.client`: debe correr también en SSR porque los composables
* lo consumen de forma síncrona en `setup()` (que se ejecuta en SSR,
* aunque los fetches reales solo disparen en cliente) con `.client` el
* valor provisto queda `undefined` en el render de servidor y todo revienta.
*/
export default defineNuxtPlugin({ export default defineNuxtPlugin({
name: 'typesense-client', name: 'typesense-client',
setup() { setup() {
const config = useRuntimeConfig() const api = useTypesenseApi()
const nodes = JSON.parse(config.public.typeSenseNodes as string)
const apiKey = config.public.typeSenseApiKey as string
const client = new Typesense.Client({
nodes,
apiKey,
numRetries: 3,
retryIntervalSeconds: 0.5,
healthcheckIntervalSeconds: 30
})
return { return {
provide: { provide: {
typesenseClient: client typesenseApi: api
} }
} }
} }

View File

@ -1,6 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
modules: ['@nuxt/eslint', '@nuxt/ui', '@vueuse/nuxt', '@nuxtjs/i18n', '@pinia/nuxt', 'nuxt-driver.js'], modules: ['@nuxt/eslint', '@nuxt/ui', '@vueuse/nuxt', '@nuxtjs/i18n', '@pinia/nuxt', '@sfxcode/nuxt-typesense','nuxt-driver.js'],
app: { app: {
head: { head: {
@ -28,9 +28,7 @@ export default defineNuxtConfig({
feedbackMaxPerSession: Number(process.env.NUXT_PUBLIC_FEEDBACK_MAX_PER_SESSION) || 3, feedbackMaxPerSession: Number(process.env.NUXT_PUBLIC_FEEDBACK_MAX_PER_SESSION) || 3,
feedbackCooldownSec: Number(process.env.NUXT_PUBLIC_FEEDBACK_COOLDOWN_SEC) || 45, feedbackCooldownSec: Number(process.env.NUXT_PUBLIC_FEEDBACK_COOLDOWN_SEC) || 45,
feedbackMinSeconds: Number(process.env.NUXT_PUBLIC_FEEDBACK_MIN_SECONDS) || 4, feedbackMinSeconds: Number(process.env.NUXT_PUBLIC_FEEDBACK_MIN_SECONDS) || 4,
entrelineasDevKey: process.env.NUXT_PUBLIC_ENTRELINEAS_DEV_KEY || '', entrelineasDevKey: process.env.NUXT_PUBLIC_ENTRELINEAS_DEV_KEY || ''
typeSenseNodes: process.env.TYPESENSE_NODES || '[]',
typeSenseApiKey: process.env.NUXT_PUBLIC_TYPESENSE_API_KEY || ''
} }
}, },
@ -89,4 +87,10 @@ export default defineNuxtConfig({
optimizeTranslationDirective: false, optimizeTranslationDirective: false,
}, },
}, },
typesense: {
url: process.env.NUXT_PUBLIC_TYPESENSE_URL || 'https://searchts.carpa.com',
apiKey: process.env.NUXT_PUBLIC_TYPESENSE_API_KEY || '',
clientMode: true
}
}) })

View File

@ -19,6 +19,7 @@
"@nuxt/ui": "^4.7.0", "@nuxt/ui": "^4.7.0",
"@nuxtjs/i18n": "^9.5.6", "@nuxtjs/i18n": "^9.5.6",
"@pinia/nuxt": "^0.11.2", "@pinia/nuxt": "^0.11.2",
"@sfxcode/nuxt-typesense": "^1.2.0",
"@tanstack/table-core": "^8.21.3", "@tanstack/table-core": "^8.21.3",
"@unovis/ts": "^1.6.5", "@unovis/ts": "^1.6.5",
"@unovis/vue": "^1.6.5", "@unovis/vue": "^1.6.5",

View File

@ -29,6 +29,9 @@ importers:
'@pinia/nuxt': '@pinia/nuxt':
specifier: ^0.11.2 specifier: ^0.11.2
version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))) version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))
'@sfxcode/nuxt-typesense':
specifier: ^1.2.0
version: 1.2.0(magicast@0.5.2)
'@tanstack/table-core': '@tanstack/table-core':
specifier: ^8.21.3 specifier: ^8.21.3
version: 8.21.3 version: 8.21.3
@ -2057,6 +2060,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@sfxcode/nuxt-typesense@1.2.0':
resolution: {integrity: sha512-5h/nc7AL4POo/RBG+M7zl2fcK1aMED+LWuP/Ob18g9rGZa4cgArLNtp+8GwYFzGIr7aZa6fH6KM+Uuu4kbXFjQ==}
'@simple-git/args-pathspec@1.0.3': '@simple-git/args-pathspec@1.0.3':
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
@ -8248,6 +8254,14 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.60.3': '@rollup/rollup-win32-x64-msvc@4.60.3':
optional: true optional: true
'@sfxcode/nuxt-typesense@1.2.0(magicast@0.5.2)':
dependencies:
'@nuxt/kit': 4.4.5(magicast@0.5.2)
consola: 3.4.2
defu: 6.1.7
transitivePeerDependencies:
- magicast
'@simple-git/args-pathspec@1.0.3': {} '@simple-git/args-pathspec@1.0.3': {}
'@simple-git/argv-parser@1.1.1': '@simple-git/argv-parser@1.1.1':