groups typesense

This commit is contained in:
David Ascanio 2026-05-20 19:54:24 -03:00
parent f2521b9ff3
commit efd752e410
3 changed files with 100 additions and 61 deletions

View File

@ -47,8 +47,38 @@ const title = computed(() =>
origin.value || (props.document?.id as string) || 'Entrelínea'
)
function formatEntrelineaText(html: string): string {
if (!html) return ''
const sPrefix = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000"
const sBracket = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000"
const sInner = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000;text-decoration:underline"
const wrapLine = (content: string, isFirst: boolean): string =>
(isFirst ? `<span style="${sPrefix}">[WSS] </span>` : '') +
`<span style="${sBracket}">«</span>` +
`<span style="${sInner}">${content}</span>` +
`<span style="${sBracket}">»</span>`
if (/<p[\s>]/i.test(html)) {
const lines = html
.split(/<p(?:[^>]*)>/i)
.map(part => part.replace(/<\/p>/gi, '').trim())
.filter(Boolean)
if (lines.length) return lines.map((l, i) => wrapLine(l, i === 0)).join('<br>')
return html
}
if (/<br/i.test(html)) {
const parts = html.split(/<br\s*\/?>/i).map(p => p.trim()).filter(Boolean)
if (parts.length) return parts.map((l, i) => wrapLine(l, i === 0)).join('<br>')
}
return wrapLine(html.trim(), true)
}
const bodyHtml = computed<string>(() =>
props.highlightedText || props.document?.text || ''
formatEntrelineaText(props.highlightedText || props.document?.text || '')
)
/* -------------------------------------------------------------------------- */

View File

@ -79,9 +79,20 @@ export interface TypesenseParagraphHit {
text_match?: number
}
interface TypesenseGroupedHit {
group_key: string[]
hits: TypesenseParagraphHit[]
}
interface TypesenseSearchResponse {
found: number
hits?: TypesenseParagraphHit[]
grouped_hits?: TypesenseGroupedHit[]
}
interface SearchGroup {
docId: string
firstHit: TypesenseParagraphHit
allHits: TypesenseParagraphHit[]
}
interface BrowseItem {
@ -97,31 +108,18 @@ interface DisplayGroup {
// ---- State ----------------------------------------------------------------
// Modo búsqueda (con query): hits de párrafos agrupados por documento
const hits = ref<TypesenseParagraphHit[]>([])
// Modo búsqueda (con query): grupos devueltos por Typesense
const groupedHits = ref<SearchGroup[]>([])
const total = ref(0)
const currentPage = ref(1)
const hasMore = computed(() =>
settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false
settings.paginationType === 'infinite_scroll' ? groupedHits.value.length < total.value : false
)
// Progressive display (sólo en scroll infinito)
const visibleGroupCount = ref(10)
const groupedHits = computed(() => {
const map = new Map<string, TypesenseParagraphHit[]>()
for (const hit of hits.value) {
const id = hit.document.document_id
if (!map.has(id)) map.set(id, [])
map.get(id)!.push(hit)
}
return [...map.entries()].map(([docId, docHits]) => ({
docId,
firstHit: docHits[0]!
}))
})
const visibleGroups = computed(() =>
settings.paginationType === 'infinite_scroll'
? groupedHits.value.slice(0, visibleGroupCount.value)
@ -243,9 +241,12 @@ async function runSearch(q: string, page = 1, append = false) {
page: typePage,
highlightFullFields: QUERY_BY,
highlightFields: QUERY_BY,
highlightFullFields: QUERY_BY,
highlightFields: QUERY_BY,
highlightStartTag: '<mark class="search-match">',
highlightEndTag: '</mark>',
snippetThreshold: 100
highlightAffixNumTokens: 30,
groupBy: 'document_id'
}]
}
})
@ -253,16 +254,21 @@ async function runSearch(q: string, page = 1, append = false) {
if (seq !== searchSeq) return
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
const newHits = res?.hits ?? []
const rawGroups = res?.grouped_hits ?? []
const newGroups: SearchGroup[] = rawGroups.map(g => ({
docId: g.group_key[0]!,
firstHit: g.hits[0]!,
allHits: g.hits
}))
if (!append) docCache.value = {}
const newDocIds = [...new Set(newHits.map(h => h.document.document_id).filter(Boolean))]
const newDocIds = newGroups.map(g => g.docId).filter(Boolean)
await fetchDocumentMeta(newDocIds)
if (seq !== searchSeq) return
hits.value = append ? hits.value.concat(newHits) : newHits
total.value = res?.found ?? hits.value.length
groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups
total.value = res?.found ?? groupedHits.value.length
currentPage.value = typePage
if (!append) activePage.value = page
} catch (err: unknown) {
@ -270,7 +276,7 @@ async function runSearch(q: string, page = 1, append = false) {
console.error('Typesense error', err)
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
if (!append) {
hits.value = []
groupedHits.value = []
total.value = 0
}
} finally {
@ -360,7 +366,7 @@ function goToPage(p: number) {
browseItems.value = []
runBrowse(p, false)
} else {
hits.value = []
groupedHits.value = []
runSearch(query.value, p, false)
}
}
@ -399,7 +405,7 @@ onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
watch(debouncedQuery, (q) => {
activePage.value = 1
if (!q.trim()) {
hits.value = []
groupedHits.value = []
total.value = 0
currentPage.value = 1
visibleGroupCount.value = 10
@ -411,7 +417,7 @@ watch(debouncedQuery, (q) => {
browseItems.value = []
browseTotal.value = 0
browsePage.value = 1
hits.value = []
groupedHits.value = []
total.value = 0
currentPage.value = 1
visibleGroupCount.value = 10
@ -498,9 +504,8 @@ async function fetchDocumentParagraphs(docId: string) {
async function selectGroup(group: DisplayGroup) {
selectedDocId.value = group.docId
selectedHit.value = group.firstHit
selectedMatchingHits.value = group.firstHit
? hits.value.filter(h => h.document.document_id === group.docId)
: []
const searchGroup = groupedHits.value.find(g => g.docId === group.docId)
selectedMatchingHits.value = searchGroup?.allHits ?? []
fetchFullDocument(group.docId)
fetchDocumentParagraphs(group.docId)
}

View File

@ -79,9 +79,20 @@ export interface TypesenseParagraphHit {
text_match?: number
}
interface TypesenseGroupedHit {
groupKey: string[]
hits: TypesenseParagraphHit[]
}
interface TypesenseSearchResponse {
found: number
hits?: TypesenseParagraphHit[]
groupedHits?: TypesenseGroupedHit[]
}
interface SearchGroup {
docId: string
firstHit: TypesenseParagraphHit
allHits: TypesenseParagraphHit[]
}
interface BrowseItem {
@ -97,31 +108,18 @@ interface DisplayGroup {
// ---- State ----------------------------------------------------------------
// Modo búsqueda (con query): hits de párrafos agrupados por documento
const hits = ref<TypesenseParagraphHit[]>([])
// Modo búsqueda (con query): grupos devueltos por Typesense
const groupedHits = ref<SearchGroup[]>([])
const total = ref(0)
const currentPage = ref(1)
const hasMore = computed(() =>
settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false
settings.paginationType === 'infinite_scroll' ? groupedHits.value.length < total.value : false
)
// Progressive display (sólo en scroll infinito)
const visibleGroupCount = ref(10)
const groupedHits = computed(() => {
const map = new Map<string, TypesenseParagraphHit[]>()
for (const hit of hits.value) {
const id = hit.document.document_id
if (!map.has(id)) map.set(id, [])
map.get(id)!.push(hit)
}
return [...map.entries()].map(([docId, docHits]) => ({
docId,
firstHit: docHits[0]!
}))
})
const visibleGroups = computed(() =>
settings.paginationType === 'infinite_scroll'
? groupedHits.value.slice(0, visibleGroupCount.value)
@ -245,24 +243,32 @@ async function runSearch(q: string, page = 1, append = false) {
highlightFields: QUERY_BY,
highlightStartTag: '<mark class="search-match">',
highlightEndTag: '</mark>',
highlightAffixNumTokens: 30
highlightAffixNumTokens: 30,
groupBy: 'document_id'
}]
}
})
console.log('Search response', multi)
if (seq !== searchSeq) return
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
const newHits = res?.hits ?? []
const rawGroups = res?.groupedHits ?? []
console.log('Raw groups', rawGroups)
const newGroups: SearchGroup[] = rawGroups.map(g => ({
docId: g.groupKey[0]!,
firstHit: g.hits[0]!,
allHits: g.hits
}))
if (!append) docCache.value = {}
const newDocIds = [...new Set(newHits.map(h => h.document.document_id).filter(Boolean))]
const newDocIds = newGroups.map(g => g.docId).filter(Boolean)
await fetchDocumentMeta(newDocIds)
if (seq !== searchSeq) return
hits.value = append ? hits.value.concat(newHits) : newHits
total.value = res?.found ?? hits.value.length
groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups
total.value = res?.found ?? groupedHits.value.length
currentPage.value = typePage
if (!append) activePage.value = page
} catch (err: unknown) {
@ -270,7 +276,7 @@ async function runSearch(q: string, page = 1, append = false) {
console.error('Typesense error', err)
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
if (!append) {
hits.value = []
groupedHits.value = []
total.value = 0
}
} finally {
@ -320,7 +326,6 @@ async function runBrowse(page = 1, append = false) {
})
if (seq !== searchSeq) return
const result = (multi?.results?.[0] as { found?: number, hits?: Array<{ document: DocMeta }> } | undefined)
const newItems = (result?.hits ?? []).map(h => ({
docId: h.document.id!,
@ -360,7 +365,7 @@ function goToPage(p: number) {
browseItems.value = []
runBrowse(p, false)
} else {
hits.value = []
groupedHits.value = []
runSearch(query.value, p, false)
}
}
@ -399,7 +404,7 @@ onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
watch(debouncedQuery, (q) => {
activePage.value = 1
if (!q.trim()) {
hits.value = []
groupedHits.value = []
total.value = 0
currentPage.value = 1
visibleGroupCount.value = 10
@ -411,7 +416,7 @@ watch(debouncedQuery, (q) => {
browseItems.value = []
browseTotal.value = 0
browsePage.value = 1
hits.value = []
groupedHits.value = []
total.value = 0
currentPage.value = 1
visibleGroupCount.value = 10
@ -498,9 +503,8 @@ async function fetchDocumentParagraphs(docId: string) {
async function selectGroup(group: DisplayGroup) {
selectedDocId.value = group.docId
selectedHit.value = group.firstHit
selectedMatchingHits.value = group.firstHit
? hits.value.filter(h => h.document.document_id === group.docId)
: []
const searchGroup = groupedHits.value.find(g => g.docId === group.docId)
selectedMatchingHits.value = searchGroup?.allHits ?? []
fetchFullDocument(group.docId)
fetchDocumentParagraphs(group.docId)
}