groups typesense
This commit is contained in:
parent
f2521b9ff3
commit
efd752e410
|
|
@ -47,8 +47,38 @@ const title = computed(() =>
|
||||||
origin.value || (props.document?.id as string) || 'Entrelínea'
|
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>(() =>
|
const bodyHtml = computed<string>(() =>
|
||||||
props.highlightedText || props.document?.text || ''
|
formatEntrelineaText(props.highlightedText || props.document?.text || '')
|
||||||
)
|
)
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,20 @@ export interface TypesenseParagraphHit {
|
||||||
text_match?: number
|
text_match?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TypesenseGroupedHit {
|
||||||
|
group_key: string[]
|
||||||
|
hits: TypesenseParagraphHit[]
|
||||||
|
}
|
||||||
|
|
||||||
interface TypesenseSearchResponse {
|
interface TypesenseSearchResponse {
|
||||||
found: number
|
found: number
|
||||||
hits?: TypesenseParagraphHit[]
|
grouped_hits?: TypesenseGroupedHit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchGroup {
|
||||||
|
docId: string
|
||||||
|
firstHit: TypesenseParagraphHit
|
||||||
|
allHits: TypesenseParagraphHit[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BrowseItem {
|
interface BrowseItem {
|
||||||
|
|
@ -97,31 +108,18 @@ interface DisplayGroup {
|
||||||
|
|
||||||
// ---- State ----------------------------------------------------------------
|
// ---- State ----------------------------------------------------------------
|
||||||
|
|
||||||
// Modo búsqueda (con query): hits de párrafos agrupados por documento
|
// Modo búsqueda (con query): grupos devueltos por Typesense
|
||||||
const hits = ref<TypesenseParagraphHit[]>([])
|
const groupedHits = ref<SearchGroup[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
const hasMore = computed(() =>
|
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)
|
// Progressive display (sólo en scroll infinito)
|
||||||
const visibleGroupCount = ref(10)
|
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(() =>
|
const visibleGroups = computed(() =>
|
||||||
settings.paginationType === 'infinite_scroll'
|
settings.paginationType === 'infinite_scroll'
|
||||||
? groupedHits.value.slice(0, visibleGroupCount.value)
|
? groupedHits.value.slice(0, visibleGroupCount.value)
|
||||||
|
|
@ -243,9 +241,12 @@ async function runSearch(q: string, page = 1, append = false) {
|
||||||
page: typePage,
|
page: typePage,
|
||||||
highlightFullFields: QUERY_BY,
|
highlightFullFields: QUERY_BY,
|
||||||
highlightFields: QUERY_BY,
|
highlightFields: QUERY_BY,
|
||||||
|
highlightFullFields: QUERY_BY,
|
||||||
|
highlightFields: QUERY_BY,
|
||||||
highlightStartTag: '<mark class="search-match">',
|
highlightStartTag: '<mark class="search-match">',
|
||||||
highlightEndTag: '</mark>',
|
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
|
if (seq !== searchSeq) return
|
||||||
|
|
||||||
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
|
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 = {}
|
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)
|
await fetchDocumentMeta(newDocIds)
|
||||||
|
|
||||||
if (seq !== searchSeq) return
|
if (seq !== searchSeq) return
|
||||||
|
|
||||||
hits.value = append ? hits.value.concat(newHits) : newHits
|
groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups
|
||||||
total.value = res?.found ?? hits.value.length
|
total.value = res?.found ?? groupedHits.value.length
|
||||||
currentPage.value = typePage
|
currentPage.value = typePage
|
||||||
if (!append) activePage.value = page
|
if (!append) activePage.value = page
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|
@ -270,7 +276,7 @@ async function runSearch(q: string, page = 1, append = false) {
|
||||||
console.error('Typesense error', err)
|
console.error('Typesense error', err)
|
||||||
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
|
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
|
||||||
if (!append) {
|
if (!append) {
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -360,7 +366,7 @@ function goToPage(p: number) {
|
||||||
browseItems.value = []
|
browseItems.value = []
|
||||||
runBrowse(p, false)
|
runBrowse(p, false)
|
||||||
} else {
|
} else {
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
runSearch(query.value, p, false)
|
runSearch(query.value, p, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,7 +405,7 @@ onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
||||||
watch(debouncedQuery, (q) => {
|
watch(debouncedQuery, (q) => {
|
||||||
activePage.value = 1
|
activePage.value = 1
|
||||||
if (!q.trim()) {
|
if (!q.trim()) {
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
visibleGroupCount.value = 10
|
visibleGroupCount.value = 10
|
||||||
|
|
@ -411,7 +417,7 @@ watch(debouncedQuery, (q) => {
|
||||||
browseItems.value = []
|
browseItems.value = []
|
||||||
browseTotal.value = 0
|
browseTotal.value = 0
|
||||||
browsePage.value = 1
|
browsePage.value = 1
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
visibleGroupCount.value = 10
|
visibleGroupCount.value = 10
|
||||||
|
|
@ -498,9 +504,8 @@ async function fetchDocumentParagraphs(docId: string) {
|
||||||
async function selectGroup(group: DisplayGroup) {
|
async function selectGroup(group: DisplayGroup) {
|
||||||
selectedDocId.value = group.docId
|
selectedDocId.value = group.docId
|
||||||
selectedHit.value = group.firstHit
|
selectedHit.value = group.firstHit
|
||||||
selectedMatchingHits.value = group.firstHit
|
const searchGroup = groupedHits.value.find(g => g.docId === group.docId)
|
||||||
? hits.value.filter(h => h.document.document_id === group.docId)
|
selectedMatchingHits.value = searchGroup?.allHits ?? []
|
||||||
: []
|
|
||||||
fetchFullDocument(group.docId)
|
fetchFullDocument(group.docId)
|
||||||
fetchDocumentParagraphs(group.docId)
|
fetchDocumentParagraphs(group.docId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,20 @@ export interface TypesenseParagraphHit {
|
||||||
text_match?: number
|
text_match?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TypesenseGroupedHit {
|
||||||
|
groupKey: string[]
|
||||||
|
hits: TypesenseParagraphHit[]
|
||||||
|
}
|
||||||
|
|
||||||
interface TypesenseSearchResponse {
|
interface TypesenseSearchResponse {
|
||||||
found: number
|
found: number
|
||||||
hits?: TypesenseParagraphHit[]
|
groupedHits?: TypesenseGroupedHit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchGroup {
|
||||||
|
docId: string
|
||||||
|
firstHit: TypesenseParagraphHit
|
||||||
|
allHits: TypesenseParagraphHit[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BrowseItem {
|
interface BrowseItem {
|
||||||
|
|
@ -97,31 +108,18 @@ interface DisplayGroup {
|
||||||
|
|
||||||
// ---- State ----------------------------------------------------------------
|
// ---- State ----------------------------------------------------------------
|
||||||
|
|
||||||
// Modo búsqueda (con query): hits de párrafos agrupados por documento
|
// Modo búsqueda (con query): grupos devueltos por Typesense
|
||||||
const hits = ref<TypesenseParagraphHit[]>([])
|
const groupedHits = ref<SearchGroup[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
const hasMore = computed(() =>
|
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)
|
// Progressive display (sólo en scroll infinito)
|
||||||
const visibleGroupCount = ref(10)
|
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(() =>
|
const visibleGroups = computed(() =>
|
||||||
settings.paginationType === 'infinite_scroll'
|
settings.paginationType === 'infinite_scroll'
|
||||||
? groupedHits.value.slice(0, visibleGroupCount.value)
|
? groupedHits.value.slice(0, visibleGroupCount.value)
|
||||||
|
|
@ -245,24 +243,32 @@ async function runSearch(q: string, page = 1, append = false) {
|
||||||
highlightFields: QUERY_BY,
|
highlightFields: QUERY_BY,
|
||||||
highlightStartTag: '<mark class="search-match">',
|
highlightStartTag: '<mark class="search-match">',
|
||||||
highlightEndTag: '</mark>',
|
highlightEndTag: '</mark>',
|
||||||
highlightAffixNumTokens: 30
|
highlightAffixNumTokens: 30,
|
||||||
|
groupBy: 'document_id'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
console.log('Search response', multi)
|
||||||
if (seq !== searchSeq) return
|
if (seq !== searchSeq) return
|
||||||
|
|
||||||
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
|
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 = {}
|
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)
|
await fetchDocumentMeta(newDocIds)
|
||||||
|
|
||||||
if (seq !== searchSeq) return
|
if (seq !== searchSeq) return
|
||||||
|
|
||||||
hits.value = append ? hits.value.concat(newHits) : newHits
|
groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups
|
||||||
total.value = res?.found ?? hits.value.length
|
total.value = res?.found ?? groupedHits.value.length
|
||||||
currentPage.value = typePage
|
currentPage.value = typePage
|
||||||
if (!append) activePage.value = page
|
if (!append) activePage.value = page
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|
@ -270,7 +276,7 @@ async function runSearch(q: string, page = 1, append = false) {
|
||||||
console.error('Typesense error', err)
|
console.error('Typesense error', err)
|
||||||
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
|
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
|
||||||
if (!append) {
|
if (!append) {
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -320,7 +326,6 @@ async function runBrowse(page = 1, append = false) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (seq !== searchSeq) return
|
if (seq !== searchSeq) return
|
||||||
|
|
||||||
const result = (multi?.results?.[0] as { found?: number, hits?: Array<{ document: DocMeta }> } | undefined)
|
const result = (multi?.results?.[0] as { found?: number, hits?: Array<{ document: DocMeta }> } | undefined)
|
||||||
const newItems = (result?.hits ?? []).map(h => ({
|
const newItems = (result?.hits ?? []).map(h => ({
|
||||||
docId: h.document.id!,
|
docId: h.document.id!,
|
||||||
|
|
@ -360,7 +365,7 @@ function goToPage(p: number) {
|
||||||
browseItems.value = []
|
browseItems.value = []
|
||||||
runBrowse(p, false)
|
runBrowse(p, false)
|
||||||
} else {
|
} else {
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
runSearch(query.value, p, false)
|
runSearch(query.value, p, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -399,7 +404,7 @@ onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
||||||
watch(debouncedQuery, (q) => {
|
watch(debouncedQuery, (q) => {
|
||||||
activePage.value = 1
|
activePage.value = 1
|
||||||
if (!q.trim()) {
|
if (!q.trim()) {
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
visibleGroupCount.value = 10
|
visibleGroupCount.value = 10
|
||||||
|
|
@ -411,7 +416,7 @@ watch(debouncedQuery, (q) => {
|
||||||
browseItems.value = []
|
browseItems.value = []
|
||||||
browseTotal.value = 0
|
browseTotal.value = 0
|
||||||
browsePage.value = 1
|
browsePage.value = 1
|
||||||
hits.value = []
|
groupedHits.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
visibleGroupCount.value = 10
|
visibleGroupCount.value = 10
|
||||||
|
|
@ -498,9 +503,8 @@ async function fetchDocumentParagraphs(docId: string) {
|
||||||
async function selectGroup(group: DisplayGroup) {
|
async function selectGroup(group: DisplayGroup) {
|
||||||
selectedDocId.value = group.docId
|
selectedDocId.value = group.docId
|
||||||
selectedHit.value = group.firstHit
|
selectedHit.value = group.firstHit
|
||||||
selectedMatchingHits.value = group.firstHit
|
const searchGroup = groupedHits.value.find(g => g.docId === group.docId)
|
||||||
? hits.value.filter(h => h.document.document_id === group.docId)
|
selectedMatchingHits.value = searchGroup?.allHits ?? []
|
||||||
: []
|
|
||||||
fetchFullDocument(group.docId)
|
fetchFullDocument(group.docId)
|
||||||
fetchDocumentParagraphs(group.docId)
|
fetchDocumentParagraphs(group.docId)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue