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 { unlocked } = useDevMode()
const toast = useToast()
const typesenseClient = useTypesenseClient()
const { documentsApi } = useTypesenseClient()
// Restaurar estado desde URL antes de crear los refs
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
@ -106,17 +106,20 @@ async function applyBibleStudyFilter() {
isValidating.value = true
try {
const res = await typesenseClient.multiSearch.perform({
searches: [{
collection: props.mainCollection,
q: '*',
query_by: 'title',
filter_by: `bible_study:=${val}`,
per_page: 1,
include_fields: 'bible_study,title',
use_cache: true,
cache_ttl: 3600
}]
const res = await documentsApi.multiSearch({
multiSearchParameters: {},
multiSearchSearchesParameter: {
searches: [{
collection: props.mainCollection,
q: '*',
queryBy: 'title',
filterBy: `bible_study:=${val}`,
perPage: 1,
includeFields: 'bible_study,title',
useCache: true,
cacheTtl: 3600
}]
}
})
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
selectedParagraphs.value = []
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 (detail) {
const rawParagraphs = detail.paragraphs as unknown as ParagraphDoc[]

View File

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

View File

@ -6,7 +6,7 @@ interface BibleStudyChip {
const activeBibleStudies = useState<BibleStudyChip[]>('bible-study-chips', () => [])
export function useFilters() {
const typesenseClient = useTypesenseClient()
const { documentsApi } = useTypesenseApi()
const toast = useToast()
const bibleStudyInput = ref<number | null>(null)
@ -25,17 +25,20 @@ export function useFilters() {
isValidating.value = true
try {
const res = await typesenseClient.multiSearch.perform({
searches: [{
collection: mainCollection,
q: '*',
query_by: 'title',
filter_by: `bible_study:=${val}`,
per_page: 1,
include_fields: 'bible_study,title',
use_cache: true,
cache_ttl: 3600
}]
const res = await documentsApi.multiSearch({
multiSearchParameters: {},
multiSearchSearchesParameter: {
searches: [{
collection: mainCollection,
q: '*',
queryBy: 'title',
filterBy: `bible_study:=${val}`,
perPage: 1,
includeFields: 'bible_study,title',
useCache: true,
cacheTtl: 3600
}]
}
})
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 detailParagraphsLoading = ref(false)
const typesenseClient = useTypesenseClient()
const { documentsApi } = useTypesenseClient()
const { fetchDocumentDetail } = useDocumentDetailFetch()
let fetchSeq = 0
@ -76,7 +76,7 @@ export function usePublicationFetch() {
detailDocument.value = null
detailParagraphs.value = []
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 (detail) {
const rawParagraphs = detail.paragraphs as unknown as ParagraphDoc[]

View File

@ -1,6 +1,9 @@
import type Client from 'typesense/Typesense/Client'
export function useTypesenseClient(): Client {
/**
* Devuelve la instancia única del cliente Typesense creada por el plugin
* `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()
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) {
const typesenseClient = useTypesenseClient()
const { documentsApi } = useTypesenseClient()
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
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
try {
const multi = await typesenseClient.multiSearch.perform({
searches: [{
collection: options.collection,
q: exactSearch.value && q ? `"${q}"` : q || '*',
query_by: options.queryBy,
include_fields: options.includeFields ?? '*',
filter_by: options.filterBy(),
per_page: options.pageSize(),
page: typePage,
highlight_full_fields: options.queryBy,
highlight_fields: options.queryBy,
highlight_start_tag: '<mark class="search-match">',
highlight_end_tag: '</mark>'
}]
}, {}, { abortSignal: signal })
const multi = await documentsApi.multiSearch({
multiSearchParameters: {},
multiSearchSearchesParameter: {
searches: [{
collection: options.collection,
q: exactSearch.value && q ? `"${q}"` : q || '*',
queryBy: options.queryBy,
includeFields: options.includeFields ?? '*',
filterBy: options.filterBy(),
perPage: options.pageSize(),
page: typePage,
highlightFullFields: options.queryBy,
highlightFields: options.queryBy,
highlightStartTag: '<mark class="search-match">',
highlightEndTag: '</mark>'
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return
@ -212,13 +215,13 @@ export interface TypesenseGroupedParagraphHit {
}
export interface TypesenseGroupedHit {
group_key: string[]
groupKey: string[]
hits: TypesenseGroupedParagraphHit[]
}
interface GroupedSearchResponse {
found: number
grouped_hits?: TypesenseGroupedHit[]
groupedHits?: TypesenseGroupedHit[]
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'
export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions) {
const typesenseClient = useTypesenseClient()
const { documentsApi } = useTypesenseClient()
const docMetaCache = useDocMetaCache()
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
const highlightAffixNumTokens = options.highlightAffixNumTokens ?? 15
@ -341,8 +344,8 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
function browseFilterBy() {
const base = options.browseFilterBy ? options.browseFilterBy() : options.filterBy()
return options.isUnlocked()
? `${base} && has_paragraphs:=true`
: `${base} && private:=false && has_paragraphs:=true`
? base
: `${base} && private:=false`
}
const runner = createAbortableRunner()
@ -364,30 +367,33 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
try {
const shouldSortByDate = sortMode.value === 'date' && q.trim()
const multi = await typesenseClient.multiSearch.perform({
searches: [{
collection: options.paragraphsCollection,
q: exactSearch.value && q ? `"${q}"` : q || '*',
query_by: options.queryBy,
filter_by: searchFilterBy(),
...(shouldSortByDate ? { sort_by: `$${options.mainCollection}(timestamp:desc)` } : {}),
per_page: options.pageSize(),
page: typePage,
highlight_full_fields: options.queryBy,
highlight_fields: options.queryBy,
highlight_start_tag: '<mark class="search-match">',
highlight_end_tag: '</mark>',
highlight_affix_num_tokens: highlightAffixNumTokens,
group_by: options.groupByField,
include_fields: `*, $${options.mainCollection}(${META_FIELDS})`
}]
}, {}, { abortSignal: signal })
const multi = await documentsApi.multiSearch({
multiSearchParameters: {},
multiSearchSearchesParameter: {
searches: [{
collection: options.paragraphsCollection,
q: exactSearch.value && q ? `"${q}"` : q || '*',
queryBy: options.queryBy,
filterBy: searchFilterBy(),
...(shouldSortByDate ? { sortBy: `$${options.mainCollection}(timestamp:desc)` } : {}),
perPage: options.pageSize(),
page: typePage,
highlightFullFields: options.queryBy,
highlightFields: options.queryBy,
highlightStartTag: '<mark class="search-match">',
highlightEndTag: '</mark>',
highlightAffixNumTokens,
groupBy: options.groupByField,
includeFields: `*, $${options.mainCollection}(${META_FIELDS})`
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return
const res = (multi?.results?.[0] ?? {}) as GroupedSearchResponse
const rawGroups = res?.grouped_hits ?? []
const rawGroups = res?.groupedHits ?? []
const newGroups: SearchGroup[] = rawGroups.map(g => ({
docId: g.group_key[0]!,
docId: g.groupKey[0]!,
firstHit: g.hits[0]!,
allHits: g.hits
}))
@ -429,20 +435,22 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
const isInfinite = options.paginationType() === 'infinite_scroll'
const typePage = isInfinite ? (append ? browsePage.value + 1 : 1) : page
console.log("testing" + browseFilterBy())
try {
const multi = await typesenseClient.multiSearch.perform({
searches: [{
collection: options.mainCollection,
q: '*',
query_by: 'title',
filter_by: browseFilterBy(),
sort_by: 'timestamp:desc',
per_page: options.pageSize(),
page: typePage,
include_fields: META_FIELDS
}]
}, {}, { abortSignal: signal })
const multi = await documentsApi.multiSearch({
multiSearchParameters: {},
multiSearchSearchesParameter: {
searches: [{
collection: options.mainCollection,
q: '*',
queryBy: 'title',
filterBy: browseFilterBy(),
sortBy: 'timestamp:desc',
perPage: options.pageSize(),
page: typePage,
includeFields: META_FIELDS
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return
const result = (multi?.results?.[0] as GroupedSearchResponse | undefined)
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({
name: 'typesense-client',
setup() {
const config = useRuntimeConfig()
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
})
const api = useTypesenseApi()
return {
provide: {
typesenseClient: client
typesenseApi: api
}
}
}

View File

@ -1,6 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
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: {
head: {
@ -28,9 +28,7 @@ export default defineNuxtConfig({
feedbackMaxPerSession: Number(process.env.NUXT_PUBLIC_FEEDBACK_MAX_PER_SESSION) || 3,
feedbackCooldownSec: Number(process.env.NUXT_PUBLIC_FEEDBACK_COOLDOWN_SEC) || 45,
feedbackMinSeconds: Number(process.env.NUXT_PUBLIC_FEEDBACK_MIN_SECONDS) || 4,
entrelineasDevKey: process.env.NUXT_PUBLIC_ENTRELINEAS_DEV_KEY || '',
typeSenseNodes: process.env.TYPESENSE_NODES || '[]',
typeSenseApiKey: process.env.NUXT_PUBLIC_TYPESENSE_API_KEY || ''
entrelineasDevKey: process.env.NUXT_PUBLIC_ENTRELINEAS_DEV_KEY || ''
}
},
@ -89,4 +87,10 @@ export default defineNuxtConfig({
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",
"@nuxtjs/i18n": "^9.5.6",
"@pinia/nuxt": "^0.11.2",
"@sfxcode/nuxt-typesense": "^1.2.0",
"@tanstack/table-core": "^8.21.3",
"@unovis/ts": "^1.6.5",
"@unovis/vue": "^1.6.5",

View File

@ -29,6 +29,9 @@ importers:
'@pinia/nuxt':
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)))
'@sfxcode/nuxt-typesense':
specifier: ^1.2.0
version: 1.2.0(magicast@0.5.2)
'@tanstack/table-core':
specifier: ^8.21.3
version: 8.21.3
@ -2057,6 +2060,9 @@ packages:
cpu: [x64]
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':
resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==}
@ -8248,6 +8254,14 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.60.3':
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/argv-parser@1.1.1':