typesense oficial library

This commit is contained in:
David Ascanio 2026-07-27 22:26:05 -03:00
parent 5751227cc3
commit 26be001eeb
10 changed files with 116 additions and 155 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 { documentsApi } = useTypesenseClient() const typesenseClient = 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,20 +106,17 @@ async function applyBibleStudyFilter() {
isValidating.value = true isValidating.value = true
try { try {
const res = await documentsApi.multiSearch({ const res = await typesenseClient.multiSearch.perform({
multiSearchParameters: {}, searches: [{
multiSearchSearchesParameter: { collection: props.mainCollection,
searches: [{ q: '*',
collection: props.mainCollection, query_by: 'title',
q: '*', filter_by: `bible_study:=${val}`,
queryBy: 'title', per_page: 1,
filterBy: `bible_study:=${val}`, include_fields: 'bible_study,title',
perPage: 1, use_cache: true,
includeFields: 'bible_study,title', cache_ttl: 3600
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]
@ -225,7 +222,7 @@ async function fetchDocumentWithParagraphs(docId: string) {
selectedDocument.value = null selectedDocument.value = null
selectedParagraphs.value = [] selectedParagraphs.value = []
try { try {
const detail = await fetchDocumentDetail(documentsApi, props.mainCollection, props.paragraphsCollection, docId, controller.signal) const detail = await fetchDocumentDetail(typesenseClient, 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(
documentsApi: ReturnType<typeof useTypesenseClient>['documentsApi'], typesenseClient: ReturnType<typeof useTypesenseClient>,
mainCollection: string, mainCollection: string,
paragraphsCollection: string, paragraphsCollection: string,
docId: string, docId: string,
@ -50,20 +50,17 @@ export function useDocumentDetailFetch() {
const cached = getCached(mainCollection, docId) const cached = getCached(mainCollection, docId)
if (cached) return cached if (cached) return cached
const res = await documentsApi.multiSearch({ const res = await typesenseClient.multiSearch.perform({
multiSearchParameters: {}, searches: [{
multiSearchSearchesParameter: { collection: mainCollection,
searches: [{ q: '*',
collection: mainCollection, query_by: 'title',
q: '*', filter_by: `id:=${docId} && $${paragraphsCollection}(id: *)`,
queryBy: 'title', include_fields: `*, $${paragraphsCollection}(*)`,
filterBy: `id:=${docId} && $${paragraphsCollection}(id: *)`, use_cache: true,
includeFields: `*, $${paragraphsCollection}(*)`, cache_ttl: 3600
useCache: true, }]
cacheTtl: 3600 }, {}, { abortSignal: signal })
}]
}
}, { 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 { documentsApi } = useTypesenseApi() const typesenseClient = useTypesenseClient()
const toast = useToast() const toast = useToast()
const bibleStudyInput = ref<number | null>(null) const bibleStudyInput = ref<number | null>(null)
@ -25,20 +25,17 @@ export function useFilters() {
isValidating.value = true isValidating.value = true
try { try {
const res = await documentsApi.multiSearch({ const res = await typesenseClient.multiSearch.perform({
multiSearchParameters: {}, searches: [{
multiSearchSearchesParameter: { collection: mainCollection,
searches: [{ q: '*',
collection: mainCollection, query_by: 'title',
q: '*', filter_by: `bible_study:=${val}`,
queryBy: 'title', per_page: 1,
filterBy: `bible_study:=${val}`, include_fields: 'bible_study,title',
perPage: 1, use_cache: true,
includeFields: 'bible_study,title', cache_ttl: 3600
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 { documentsApi } = useTypesenseClient() const typesenseClient = 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(documentsApi, config.main, config.paragraphs, docId, controller.signal) const detail = await fetchDocumentDetail(typesenseClient, 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,9 +1,6 @@
/** import type Client from 'typesense/Typesense/Client'
* Devuelve la instancia única del cliente Typesense creada por el plugin
* `typesense-client.client.ts`, en vez de construir un `Configuration` + export function useTypesenseClient(): Client {
* 14 sub-APIs nuevos en cada llamada (lo que hace `useTypesenseApi()` crudo).
*/
export function useTypesenseClient() {
const nuxtApp = useNuxtApp() const nuxtApp = useNuxtApp()
return nuxtApp.$typesenseApi return nuxtApp.$typesenseClient as Client
} }

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 { documentsApi } = useTypesenseClient() const typesenseClient = 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,24 +116,21 @@ 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 documentsApi.multiSearch({ const multi = await typesenseClient.multiSearch.perform({
multiSearchParameters: {}, searches: [{
multiSearchSearchesParameter: { collection: options.collection,
searches: [{ q: exactSearch.value && q ? `"${q}"` : q || '*',
collection: options.collection, query_by: options.queryBy,
q: exactSearch.value && q ? `"${q}"` : q || '*', include_fields: options.includeFields ?? '*',
queryBy: options.queryBy, filter_by: options.filterBy(),
includeFields: options.includeFields ?? '*', per_page: options.pageSize(),
filterBy: options.filterBy(), page: typePage,
perPage: options.pageSize(), highlight_full_fields: options.queryBy,
page: typePage, highlight_fields: options.queryBy,
highlightFullFields: options.queryBy, highlight_start_tag: '<mark class="search-match">',
highlightFields: options.queryBy, highlight_end_tag: '</mark>'
highlightStartTag: '<mark class="search-match">', }]
highlightEndTag: '</mark>' }, {}, { abortSignal: signal })
}]
}
}, { signal })
if (!runner.isCurrent(seq)) return if (!runner.isCurrent(seq)) return
@ -215,13 +212,13 @@ export interface TypesenseGroupedParagraphHit {
} }
export interface TypesenseGroupedHit { export interface TypesenseGroupedHit {
groupKey: string[] group_key: string[]
hits: TypesenseGroupedParagraphHit[] hits: TypesenseGroupedParagraphHit[]
} }
interface GroupedSearchResponse { interface GroupedSearchResponse {
found: number found: number
groupedHits?: TypesenseGroupedHit[] grouped_hits?: TypesenseGroupedHit[]
hits?: Array<{ document: Record<string, unknown> }> hits?: Array<{ document: Record<string, unknown> }>
} }
@ -263,7 +260,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 { documentsApi } = useTypesenseClient() const typesenseClient = 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
@ -344,8 +341,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 ? `${base} && has_paragraphs:=true`
: `${base} && private:=false` : `${base} && private:=false && has_paragraphs:=true`
} }
const runner = createAbortableRunner() const runner = createAbortableRunner()
@ -367,33 +364,30 @@ export function useGroupedTypesenseSearch(options: GroupedTypesenseSearchOptions
try { try {
const shouldSortByDate = sortMode.value === 'date' && q.trim() const shouldSortByDate = sortMode.value === 'date' && q.trim()
const multi = await documentsApi.multiSearch({ const multi = await typesenseClient.multiSearch.perform({
multiSearchParameters: {}, searches: [{
multiSearchSearchesParameter: { collection: options.paragraphsCollection,
searches: [{ q: exactSearch.value && q ? `"${q}"` : q || '*',
collection: options.paragraphsCollection, query_by: options.queryBy,
q: exactSearch.value && q ? `"${q}"` : q || '*', filter_by: searchFilterBy(),
queryBy: options.queryBy, ...(shouldSortByDate ? { sort_by: `$${options.mainCollection}(timestamp:desc)` } : {}),
filterBy: searchFilterBy(), per_page: options.pageSize(),
...(shouldSortByDate ? { sortBy: `$${options.mainCollection}(timestamp:desc)` } : {}), page: typePage,
perPage: options.pageSize(), highlight_full_fields: options.queryBy,
page: typePage, highlight_fields: options.queryBy,
highlightFullFields: options.queryBy, highlight_start_tag: '<mark class="search-match">',
highlightFields: options.queryBy, highlight_end_tag: '</mark>',
highlightStartTag: '<mark class="search-match">', highlight_affix_num_tokens: highlightAffixNumTokens,
highlightEndTag: '</mark>', group_by: options.groupByField,
highlightAffixNumTokens, include_fields: `*, $${options.mainCollection}(${META_FIELDS})`
groupBy: options.groupByField, }]
includeFields: `*, $${options.mainCollection}(${META_FIELDS})` }, {}, { abortSignal: signal })
}]
}
}, { 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?.groupedHits ?? [] const rawGroups = res?.grouped_hits ?? []
const newGroups: SearchGroup[] = rawGroups.map(g => ({ const newGroups: SearchGroup[] = rawGroups.map(g => ({
docId: g.groupKey[0]!, docId: g.group_key[0]!,
firstHit: g.hits[0]!, firstHit: g.hits[0]!,
allHits: g.hits allHits: g.hits
})) }))
@ -435,22 +429,20 @@ 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 documentsApi.multiSearch({ const multi = await typesenseClient.multiSearch.perform({
multiSearchParameters: {}, searches: [{
multiSearchSearchesParameter: { collection: options.mainCollection,
searches: [{ q: '*',
collection: options.mainCollection, query_by: 'title',
q: '*', filter_by: browseFilterBy(),
queryBy: 'title', sort_by: 'timestamp:desc',
filterBy: browseFilterBy(), per_page: options.pageSize(),
sortBy: 'timestamp:desc', page: typePage,
perPage: options.pageSize(), include_fields: META_FIELDS
page: typePage, }]
includeFields: META_FIELDS }, {}, { abortSignal: signal })
}]
}
}, { 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 api = useTypesenseApi() 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
})
return { return {
provide: { provide: {
typesenseApi: api typesenseClient: client
} }
} }
} }

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', '@sfxcode/nuxt-typesense','nuxt-driver.js'], modules: ['@nuxt/eslint', '@nuxt/ui', '@vueuse/nuxt', '@nuxtjs/i18n', '@pinia/nuxt', 'nuxt-driver.js'],
app: { app: {
head: { head: {
@ -28,7 +28,9 @@ 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 || ''
} }
}, },
@ -87,10 +89,4 @@ 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,7 +19,6 @@
"@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,9 +29,6 @@ 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
@ -2060,9 +2057,6 @@ 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==}
@ -8254,14 +8248,6 @@ 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':