Skip to content

Commit 9d709c7

Browse files
authored
impr(quote search): add exact search quotes (@Leonabcd123) (monkeytypegame#7261)
### Description Make text wrapped in `""` required when searching for quotes (meaning only quotes that contain this exact text will appear in the results). This allows case insensitivity (so "hello" will match "Hello").
1 parent 8d1eefc commit 9d709c7

File tree

2 files changed

+82
-12
lines changed

2 files changed

+82
-12
lines changed

frontend/src/ts/modals/quote-search.ts

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ const searchServiceCache: Record<string, SearchService<Quote>> = {};
2828
const pageSize = 100;
2929
let currentPageNumber = 1;
3030
let usingCustomLength = true;
31+
let quotes: Quote[];
32+
33+
async function updateQuotes(): Promise<void> {
34+
({ quotes } = await QuotesController.getQuotes(Config.language));
35+
}
3136

3237
function getSearchService<T>(
3338
language: string,
@@ -188,10 +193,61 @@ function buildQuoteSearchResult(
188193
`;
189194
}
190195

196+
function exactSearch(quotes: Quote[], captured: RegExp[]): [Quote[], string[]] {
197+
const matches: Quote[] = [];
198+
const exactSearchQueryTerms: Set<string> = new Set<string>();
199+
200+
for (const quote of quotes) {
201+
const textAndSource = quote.text + quote.source;
202+
const currentMatches = [];
203+
let noMatch = false;
204+
205+
for (const regex of captured) {
206+
const match = textAndSource.match(regex);
207+
208+
if (!match) {
209+
noMatch = true;
210+
break;
211+
}
212+
213+
currentMatches.push(match[0]);
214+
}
215+
216+
if (!noMatch) {
217+
currentMatches.forEach((match) => exactSearchQueryTerms.add(match));
218+
matches.push(quote);
219+
}
220+
}
221+
222+
return [matches, Array.from(exactSearchQueryTerms)];
223+
}
224+
191225
async function updateResults(searchText: string): Promise<void> {
192226
if (!modal.isOpen()) return;
193227

194-
const { quotes } = await QuotesController.getQuotes(Config.language);
228+
if (quotes === undefined) {
229+
({ quotes } = await QuotesController.getQuotes(Config.language));
230+
}
231+
232+
let matches: Quote[] = [];
233+
let matchedQueryTerms: string[] = [];
234+
let exactSearchMatches: Quote[] = [];
235+
let exactSearchMatchedQueryTerms: string[] = [];
236+
237+
const quotationsRegex = /"(.*?)"/g;
238+
const exactSearchQueries = Array.from(searchText.matchAll(quotationsRegex));
239+
const removedSearchText = searchText.replaceAll(quotationsRegex, "");
240+
241+
if (exactSearchQueries[0]) {
242+
const searchQueriesRaw = exactSearchQueries.map(
243+
(query) => new RegExp(query[1] ?? "", "i"),
244+
);
245+
246+
[exactSearchMatches, exactSearchMatchedQueryTerms] = exactSearch(
247+
quotes,
248+
searchQueriesRaw,
249+
);
250+
}
195251

196252
const quoteSearchService = getSearchService<Quote>(
197253
Config.language,
@@ -200,8 +256,21 @@ async function updateResults(searchText: string): Promise<void> {
200256
return `${quote.text} ${quote.id} ${quote.source}`;
201257
},
202258
);
203-
const { results: matches, matchedQueryTerms } =
204-
quoteSearchService.query(searchText);
259+
260+
if (exactSearchMatches.length > 0 || removedSearchText === searchText) {
261+
const ids = exactSearchMatches.map((match) => match.id);
262+
263+
({ results: matches, matchedQueryTerms } = quoteSearchService.query(
264+
removedSearchText,
265+
ids,
266+
));
267+
268+
exactSearchMatches.forEach((match) => {
269+
if (!matches.includes(match)) matches.push(match);
270+
});
271+
272+
matchedQueryTerms = [...exactSearchMatchedQueryTerms, ...matchedQueryTerms];
273+
}
205274

206275
const quotesToShow = applyQuoteLengthFilter(
207276
applyQuoteFavFilter(searchText === "" ? quotes : matches),
@@ -340,12 +409,7 @@ export async function show(showOptions?: ShowOptions): Promise<void> {
340409
});
341410
},
342411
afterAnimation: async () => {
343-
const quoteSearchInputValue = $(
344-
"#quoteSearchModal input",
345-
).val() as string;
346-
currentPageNumber = 1;
347-
348-
void updateResults(quoteSearchInputValue);
412+
void updateQuotes();
349413
},
350414
});
351415
}

frontend/src/ts/utils/search-service.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { stemmer } from "stemmer";
22
import levenshtein from "damerau-levenshtein";
33

44
export type SearchService<T> = {
5-
query: (query: string) => SearchResult<T>;
5+
query: (query: string, ids: number[]) => SearchResult<T>;
66
};
77

88
type SearchServiceOptions = {
@@ -110,7 +110,7 @@ export const buildSearchService = <T>(
110110

111111
const tokenSet = Object.keys(reverseIndex);
112112

113-
const query = (searchQuery: string): SearchResult<T> => {
113+
const query = (searchQuery: string, ids: number[]): SearchResult<T> => {
114114
const searchResult: SearchResult<T> = {
115115
results: [],
116116
matchedQueryTerms: [],
@@ -155,7 +155,13 @@ export const buildSearchService = <T>(
155155

156156
const scoreForToken = score * idf * termFrequency;
157157

158-
results.set(document.id, currentScore + scoreForToken);
158+
const quote = documents[document.id] as InternalDocument;
159+
if (
160+
ids.length === 0 ||
161+
(quote !== null && quote !== undefined && ids.includes(quote.id))
162+
) {
163+
results.set(document.id, currentScore + scoreForToken);
164+
}
159165
});
160166

161167
normalizedTokenToOriginal[token]?.forEach((originalToken) => {

0 commit comments

Comments
 (0)