From dfc524e8881990bac181f8632f110d1938526956 Mon Sep 17 00:00:00 2001 From: Valentyne Stigloher Date: Thu, 19 Dec 2024 18:09:04 +0100 Subject: [PATCH] (refactor) common SearchDocument type and component for all search categories --- components/search/SearchItem.vue | 67 +++++ components/search/SearchItemBlog.vue | 45 ---- components/search/SearchItemFaq.vue | 17 -- components/search/SearchItemInclusive.vue | 17 -- components/search/SearchItemLink.vue | 17 -- components/search/SearchItemNoun.vue | 17 -- components/search/SearchItemPronoun.vue | 17 -- components/search/SearchItemSource.vue | 29 -- components/search/SearchItemTerm.vue | 29 -- pages/search.vue | 15 +- server/api/search.get.ts | 309 ++++------------------ src/search.ts | 17 ++ 12 files changed, 147 insertions(+), 449 deletions(-) create mode 100644 components/search/SearchItem.vue delete mode 100644 components/search/SearchItemBlog.vue delete mode 100644 components/search/SearchItemFaq.vue delete mode 100644 components/search/SearchItemInclusive.vue delete mode 100644 components/search/SearchItemLink.vue delete mode 100644 components/search/SearchItemNoun.vue delete mode 100644 components/search/SearchItemPronoun.vue delete mode 100644 components/search/SearchItemSource.vue delete mode 100644 components/search/SearchItemTerm.vue create mode 100644 src/search.ts diff --git a/components/search/SearchItem.vue b/components/search/SearchItem.vue new file mode 100644 index 000000000..a0067b1fb --- /dev/null +++ b/components/search/SearchItem.vue @@ -0,0 +1,67 @@ + + + diff --git a/components/search/SearchItemBlog.vue b/components/search/SearchItemBlog.vue deleted file mode 100644 index 6c12711d5..000000000 --- a/components/search/SearchItemBlog.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/components/search/SearchItemFaq.vue b/components/search/SearchItemFaq.vue deleted file mode 100644 index 6bb0253d9..000000000 --- a/components/search/SearchItemFaq.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/components/search/SearchItemInclusive.vue b/components/search/SearchItemInclusive.vue deleted file mode 100644 index bb665c911..000000000 --- a/components/search/SearchItemInclusive.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/components/search/SearchItemLink.vue b/components/search/SearchItemLink.vue deleted file mode 100644 index 3fd2b76fa..000000000 --- a/components/search/SearchItemLink.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/components/search/SearchItemNoun.vue b/components/search/SearchItemNoun.vue deleted file mode 100644 index 7247c5e9b..000000000 --- a/components/search/SearchItemNoun.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/components/search/SearchItemPronoun.vue b/components/search/SearchItemPronoun.vue deleted file mode 100644 index 53e95ae38..000000000 --- a/components/search/SearchItemPronoun.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/components/search/SearchItemSource.vue b/components/search/SearchItemSource.vue deleted file mode 100644 index adda62972..000000000 --- a/components/search/SearchItemSource.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/components/search/SearchItemTerm.vue b/components/search/SearchItemTerm.vue deleted file mode 100644 index 5d682e554..000000000 --- a/components/search/SearchItemTerm.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/pages/search.vue b/pages/search.vue index 202229f5c..acd21b2ed 100644 --- a/pages/search.vue +++ b/pages/search.vue @@ -31,15 +31,12 @@ const searchInput = useTemplateRef('searchInput');
    -
  • - - - - - - - - +
  • +
diff --git a/server/api/search.get.ts b/server/api/search.get.ts index 13aa21933..bde7a7da2 100644 --- a/server/api/search.get.ts +++ b/server/api/search.get.ts @@ -6,7 +6,7 @@ import MiniSearch from 'minisearch'; import type { MatchInfo, SearchResult } from 'minisearch'; import type { Config } from '~/locale/config.ts'; -import { getPosts, type PostMetadata } from '~/server/blog.ts'; +import { getPosts } from '~/server/blog.ts'; import { getInclusiveEntries } from '~/server/express/inclusive.ts'; import { getNounEntries } from '~/server/express/nouns.ts'; import { getSourcesEntries } from '~/server/express/sources.ts'; @@ -17,6 +17,7 @@ import { parsePronouns } from '~/src/buildPronoun.ts'; import { genders, gendersWithNumerus } from '~/src/classes.ts'; import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts'; import parseMarkdown from '~/src/parseMarkdown.ts'; +import type { SearchDocument } from '~/src/search.ts'; import { Translator } from '~/src/translator.ts'; import { loadTsv } from '~/src/tsv.ts'; @@ -60,14 +61,16 @@ const highlightMatches = (field: string, terms: string[] | undefined, fragment: return field.replaceAll(termsRegex, `$1`); }; -abstract class SearchIndex { - documents: D[]; - index: MiniSearch; +abstract class SearchIndex { + documents: SearchDocument[]; + index: MiniSearch; - protected constructor(fields: (keyof D)[]) { + abstract TYPE: SearchDocument['type']; + + constructor() { this.documents = []; this.index = new MiniSearch({ - fields: fields as string[], + fields: ['url', 'title', 'titleSmall', 'content'], storeFields: ['type'], }); } @@ -77,36 +80,31 @@ abstract class SearchIndex { this.index.addAll(this.documents); } - abstract getDocuments(config: Config): Promise; + abstract getDocuments(config: Config): Promise; - abstract transform(result: SearchResult): R; -} - -interface SearchDocumentPronoun { - id: number; - type: SearchIndexPronoun['TYPE']; - url: string; - short: string; - small: string | undefined; - content: string; -} - -export type SearchResultPronoun = SearchDocumentPronoun; - -class SearchIndexPronoun extends SearchIndex { - TYPE = 'pronoun' as const; - - constructor() { - super(['url', 'short', 'small', 'content']); + transform(result: SearchResult): SearchDocument { + const document = this.documents[result.id]; + const termsByField = getTermsByField(result.match); + const transformed = structuredClone(document); + transformed.title = highlightMatches(document.title, termsByField.title); + transformed.content = highlightMatches(document.content, termsByField.content, true); + this.transformAdditionalFields(transformed, termsByField); + return transformed; } - async getDocuments(config: Config): Promise { + transformAdditionalFields(_transformed: SearchDocument, _termsByField: Record) {} +} + +class SearchIndexPronoun extends SearchIndex { + TYPE = 'pronoun' as const; + + async getDocuments(config: Config): Promise { if (!config.pronouns.enabled) { return []; } const pronouns = parsePronouns(config, loadTsv(`${rootDir}/data/pronouns/pronouns.tsv`)); - return Object.values(pronouns).map((pronoun, id): SearchDocumentPronoun => { + return Object.values(pronouns).map((pronoun, id): SearchDocument => { const description = Array.isArray(pronoun.description) ? pronoun.description.join() : pronoun.description; @@ -120,45 +118,26 @@ class SearchIndexPronoun extends SearchIndex) { + transformed.title = `${transformed.title}`; + if (transformed.titleSmall) { + transformed.title += `/${highlightMatches(transformed.titleSmall, termsByField.titleSmall)}`; + delete transformed.titleSmall; + } } } -interface SearchDocumentNoun { - id: number; - type: SearchIndexNoun['TYPE']; - url: string; - title: string; - content: string; -} - -export type SearchResultNoun = SearchDocumentNoun; - -class SearchIndexNoun extends SearchIndex { +class SearchIndexNoun extends SearchIndex { TYPE = 'noun' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.nouns.enabled) { return []; } @@ -167,7 +146,7 @@ class SearchIndexNoun extends SearchIndex const db = useDatabase(); const nouns = await getNounEntries(db, () => false); - return nouns.map((noun, id): SearchDocumentNoun => { + return nouns.map((noun, id): SearchDocument => { const firstWords = genders .filter((gender) => noun[gender]) .map((gender) => noun[gender].split('|')[0]); @@ -183,39 +162,12 @@ class SearchIndexNoun extends SearchIndex }; }); } - - override transform(result: SearchResult): SearchResultNoun { - const document = this.documents[result.id]; - const termsByField = getTermsByField(result.match); - return { - id: document.id, - type: document.type, - url: document.url, - title: highlightMatches(document.title, termsByField.title), - content: highlightMatches(document.content, termsByField.content, true), - }; - } } -interface SearchDocumentSource { - id: number; - type: SearchIndexSource['TYPE']; - url: string; - title: string; - image: string | undefined; - content: string; -} - -export type SearchResultSource = SearchDocumentSource; - -class SearchIndexSource extends SearchIndex { +class SearchIndexSource extends SearchIndex { TYPE = 'source' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.sources.enabled) { return []; } @@ -226,7 +178,7 @@ class SearchIndexSource extends SearchIndex false, undefined); - return sources.map((source, id): SearchDocumentSource => { + return sources.map((source, id): SearchDocument => { let title = ''; if (source.author) { title += `${source.author.replace('^', '')} – `; @@ -257,44 +209,17 @@ class SearchIndexSource extends SearchIndex { +class SearchIndexLink extends SearchIndex { TYPE = 'link' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.links.enabled) { return []; } @@ -310,38 +235,12 @@ class SearchIndexLink extends SearchIndex }; }); } - - override transform(result: SearchResult): SearchResultLink { - const document = this.documents[result.id]; - const termsByField = getTermsByField(result.match); - return { - id: document.id, - type: document.type, - url: document.url, - title: highlightMatches(document.title, termsByField.title), - content: highlightMatches(document.content, termsByField.content, true), - }; - } } -interface SearchDocumentFaq { - id: number; - type: SearchIndexFaq['TYPE']; - url: string; - title: string; - content: string; -} - -export type SearchResultFaq = SearchDocumentFaq; - -class SearchIndexFaq extends SearchIndex { +class SearchIndexFaq extends SearchIndex { TYPE = 'faq' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.faq.enabled) { return []; } @@ -357,42 +256,17 @@ class SearchIndexFaq extends SearchIndex { }; }); } - - override transform(result: SearchResult): SearchResultFaq { - const document = this.documents[result.id]; - const termsByField = getTermsByField(result.match); - return { - id: document.id, - type: document.type, - url: document.url, - title: highlightMatches(document.title, termsByField.title), - content: highlightMatches(document.content, termsByField.content, true), - }; - } } -interface SearchDocumentBlog extends PostMetadata { - id: number; - type: SearchIndexBlog['TYPE']; - url: string; - content: string; -} - -export type SearchResultBlog = SearchDocumentBlog; - -class SearchIndexBlog extends SearchIndex { +class SearchIndexBlog extends SearchIndex { TYPE = 'blog' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.links.enabled || !config.links.blog) { return []; } - const documents: SearchDocumentBlog[] = []; + const documents: SearchDocument[] = []; for (const post of (await getPosts())) { const content = await fs.readFile(`${rootDir}/data/blog/${post.slug}.md`, 'utf-8'); // exclude title, date and author from searchable content @@ -408,49 +282,19 @@ class SearchIndexBlog extends SearchIndex title: post.title, date: post.date, authors: post.authors, - hero: post.hero, + image: post.hero, content: text, }); } } return documents; } - - override transform(result: SearchResult): SearchResultBlog { - const document = this.documents[result.id]; - const termsByField = getTermsByField(result.match); - return { - id: document.id, - type: document.type, - url: document.url, - title: highlightMatches(document.title, termsByField.title), - date: document.date, - authors: document.authors, - hero: document.hero, - content: highlightMatches(document.content, termsByField.content, true), - }; - } } -interface SearchDocumentTerm { - id: number; - type: SearchIndexTerm['TYPE']; - url: string; - title: string; - image: string | undefined; - content: string; -} - -export type SearchResultTerm = SearchDocumentTerm; - -class SearchIndexTerm extends SearchIndex { +class SearchIndexTerm extends SearchIndex { TYPE = 'term' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.terminology.enabled) { return []; } @@ -461,7 +305,7 @@ class SearchIndexTerm extends SearchIndex const db = useDatabase(); const terms = await getTermsEntries(db, () => false); - return terms.map((term, id): SearchDocumentTerm => { + return terms.map((term, id): SearchDocument => { const title = term.term.replaceAll('|', ', '); let content = ''; @@ -483,44 +327,17 @@ class SearchIndexTerm extends SearchIndex type: this.TYPE, url: `/${base}?filter=${term.key}`, title, - image, + image: image ? { src: image } : undefined, content, }; }); } - - override transform(result: SearchResult): SearchResultTerm { - const document = this.documents[result.id]; - const termsByField = getTermsByField(result.match); - return { - id: document.id, - type: document.type, - url: document.url, - title: highlightMatches(document.title, termsByField.title), - image: document.image, - content: highlightMatches(document.content, termsByField.content, true), - }; - } } -interface SearchDocumentInclusive { - id: number; - type: SearchIndexInclusive['TYPE']; - url: string; - title: string; - content: string; -} - -export type SearchResultInclusive = SearchDocumentInclusive; - -class SearchIndexInclusive extends SearchIndex { +class SearchIndexInclusive extends SearchIndex { TYPE = 'inclusive' as const; - constructor() { - super(['url', 'title', 'content']); - } - - async getDocuments(config: Config): Promise { + async getDocuments(config: Config): Promise { if (!config.inclusive.enabled) { return []; } @@ -529,7 +346,7 @@ class SearchIndexInclusive extends SearchIndex false); - return inclusiveEntries.map((inclusiveEntry, id): SearchDocumentInclusive => { + return inclusiveEntries.map((inclusiveEntry, id): SearchDocument => { const insteadOf = inclusiveEntry.insteadOf.split('|'); const say = inclusiveEntry.say?.split('|') ?? []; let content = `${translator.translate('inclusive.insteadOf')}: ${insteadOf.join(', ')}` + @@ -548,18 +365,6 @@ class SearchIndexInclusive extends SearchIndex { @@ -587,6 +392,6 @@ export default defineEventHandler(async (event) => { return resultB.score - resultA.score; }) .map((result) => { - return indices[result.type as keyof typeof indices].transform(result); + return indices[result.type].transform(result); }); }); diff --git a/src/search.ts b/src/search.ts new file mode 100644 index 000000000..d3b5ce496 --- /dev/null +++ b/src/search.ts @@ -0,0 +1,17 @@ +interface SearchDocumentImage { + src: string; + alt?: string; + class?: string; +} + +export interface SearchDocument { + id: number; + type: 'pronoun' | 'noun' | 'source' | 'link' | 'faq' | 'blog' | 'term' | 'inclusive'; + url: string; + title: string; + titleSmall?: string | undefined; + image?: SearchDocumentImage | undefined; + content: string; + date?: string; + authors?: string[]; +}