diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index 14e7c043a..ddfacc1bb 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -28,6 +28,11 @@ const searchServiceCache: Record> = {}; const pageSize = 100; let currentPageNumber = 1; let usingCustomLength = true; +let quotes: Quote[]; + +async function updateQuotes(): Promise { + ({ quotes } = await QuotesController.getQuotes(Config.language)); +} function getSearchService( language: string, @@ -188,10 +193,61 @@ function buildQuoteSearchResult( `; } +function exactSearch(quotes: Quote[], captured: RegExp[]): [Quote[], string[]] { + const matches: Quote[] = []; + const exactSearchQueryTerms: Set = new Set(); + + for (const quote of quotes) { + const textAndSource = quote.text + quote.source; + const currentMatches = []; + let noMatch = false; + + for (const regex of captured) { + const match = textAndSource.match(regex); + + if (!match) { + noMatch = true; + break; + } + + currentMatches.push(match[0]); + } + + if (!noMatch) { + currentMatches.forEach((match) => exactSearchQueryTerms.add(match)); + matches.push(quote); + } + } + + return [matches, Array.from(exactSearchQueryTerms)]; +} + async function updateResults(searchText: string): Promise { if (!modal.isOpen()) return; - const { quotes } = await QuotesController.getQuotes(Config.language); + if (quotes === undefined) { + ({ quotes } = await QuotesController.getQuotes(Config.language)); + } + + let matches: Quote[] = []; + let matchedQueryTerms: string[] = []; + let exactSearchMatches: Quote[] = []; + let exactSearchMatchedQueryTerms: string[] = []; + + const quotationsRegex = /"(.*?)"/g; + const exactSearchQueries = Array.from(searchText.matchAll(quotationsRegex)); + const removedSearchText = searchText.replaceAll(quotationsRegex, ""); + + if (exactSearchQueries[0]) { + const searchQueriesRaw = exactSearchQueries.map( + (query) => new RegExp(query[1] ?? "", "i"), + ); + + [exactSearchMatches, exactSearchMatchedQueryTerms] = exactSearch( + quotes, + searchQueriesRaw, + ); + } const quoteSearchService = getSearchService( Config.language, @@ -200,8 +256,21 @@ async function updateResults(searchText: string): Promise { return `${quote.text} ${quote.id} ${quote.source}`; }, ); - const { results: matches, matchedQueryTerms } = - quoteSearchService.query(searchText); + + if (exactSearchMatches.length > 0 || removedSearchText === searchText) { + const ids = exactSearchMatches.map((match) => match.id); + + ({ results: matches, matchedQueryTerms } = quoteSearchService.query( + removedSearchText, + ids, + )); + + exactSearchMatches.forEach((match) => { + if (!matches.includes(match)) matches.push(match); + }); + + matchedQueryTerms = [...exactSearchMatchedQueryTerms, ...matchedQueryTerms]; + } const quotesToShow = applyQuoteLengthFilter( applyQuoteFavFilter(searchText === "" ? quotes : matches), @@ -340,12 +409,7 @@ export async function show(showOptions?: ShowOptions): Promise { }); }, afterAnimation: async () => { - const quoteSearchInputValue = $( - "#quoteSearchModal input", - ).val() as string; - currentPageNumber = 1; - - void updateResults(quoteSearchInputValue); + void updateQuotes(); }, }); } diff --git a/frontend/src/ts/utils/search-service.ts b/frontend/src/ts/utils/search-service.ts index 2773f3c29..8a57203fd 100644 --- a/frontend/src/ts/utils/search-service.ts +++ b/frontend/src/ts/utils/search-service.ts @@ -2,7 +2,7 @@ import { stemmer } from "stemmer"; import levenshtein from "damerau-levenshtein"; export type SearchService = { - query: (query: string) => SearchResult; + query: (query: string, ids: number[]) => SearchResult; }; type SearchServiceOptions = { @@ -110,7 +110,7 @@ export const buildSearchService = ( const tokenSet = Object.keys(reverseIndex); - const query = (searchQuery: string): SearchResult => { + const query = (searchQuery: string, ids: number[]): SearchResult => { const searchResult: SearchResult = { results: [], matchedQueryTerms: [], @@ -155,7 +155,13 @@ export const buildSearchService = ( const scoreForToken = score * idf * termFrequency; - results.set(document.id, currentScore + scoreForToken); + const quote = documents[document.id] as InternalDocument; + if ( + ids.length === 0 || + (quote !== null && quote !== undefined && ids.includes(quote.id)) + ) { + results.set(document.id, currentScore + scoreForToken); + } }); normalizedTokenToOriginal[token]?.forEach((originalToken) => {