nm3clol-express-app/app/search/router.mts

189 lines
8.7 KiB
TypeScript

console.log(`Loading nm3clol-express-app search router module...`);
import express from 'express';
import { parse, toString } from 'lucene';
import { createClient, Query } from 'solr-client';
import { SearchResponse } from 'solr-client/dist/lib/solr.js';
import { config } from '../config.mjs';
import helpers from '../helpers/functions.mjs';
import { Breadcrumb } from '../helpers/breadcrumbs.mjs';
interface Dictionary<T> {
[Key: string]: T;
}
interface Highlight {
text: string[];
}
interface WithHighlighting {
highlighting: Dictionary<Highlight>;
}
export default function () {
const searchRouter = express.Router();
searchRouter.get('/', (req: express.Request, res: express.Response) => {
// Extract paging parameters from request query parameters
let { q = '', page = 1, pageSize = 10 } = req.query;
// Sanitize query, with particular emphasis on one problem area where soft keyboards are creating fancy quotes but we need basic quotes
if (typeof q != "undefined") {
if (typeof q != "string") {
q = (q as string[]).join(' ');
}
q = q?.replaceAll(/[“”“”„„‟❝❞〝〞〟"❠⹂🙶🙷🙸]/g, '\"').replaceAll(/[‘’‘’'‚‛❛❜❟]/g, '\'');
}
if (page instanceof String) page = parseInt(page as string);
if (pageSize instanceof String) pageSize = parseInt(pageSize as string);
// Cap at 100 max per page
pageSize = Math.min(pageSize as number, 100);
// Calculate start offset for pagination
const start = (page as number - 1) * pageSize;
if (!q || (typeof q === 'string' && q.trim() == "")) {
// Build breadcrumbs
const breadcrumbs: Breadcrumb[] = [
{ title: `${config.siteName}`, url: '/' },
{ title: `Search Error`, url: req.url }
];
// Render ejs page to output
res.render('search-error', { breadcrumbs, h: helpers, query: q, error: { code: 400, message: 'Search query is required.'} });
}
else {
// Parse query
let parsedQuery = parse(q);
// Construct a Solr q field query string based on the extracted components
let qQuery = toString(parsedQuery);
// Generate a Solr query based on the query strings and additional parameters
let solrQuery = new Query().df('text').q(qQuery).start(start).rows(10).hl({
on: true,
q: qQuery,
fl: '*',
snippets: 5,
formatter: 'simple',
simplePre: `<b class="result-highlight">`,
simplePost: `</b>`,
highlightMultiTerm: true,
usePhraseHighlighter: true,
});
// Create a Solr client
const solrClient = createClient({ host: config.solrDocsHost, port: config.solrDocsPort, core: config.solrDocsCore });
solrClient.search(solrQuery)
.then((solrResponse: SearchResponse<unknown>|WithHighlighting) => {
const solrResponseAsSearchResponse = solrResponse as SearchResponse<unknown>;
const solrResponseWithHighlighting = solrResponse as WithHighlighting;
//console.log(require('util').inspect(solrResponse, { showHidden: true, depth: null, colors: true }));
// overcome broken hl simplePre/simplePost implementation
let overrideHighlighting: Dictionary<Highlight> = {};
Object.keys(solrResponseWithHighlighting.highlighting).forEach((highlight_key: string) => {
overrideHighlighting[highlight_key] = solrResponseWithHighlighting.highlighting[highlight_key];
if (overrideHighlighting[highlight_key].text && overrideHighlighting[highlight_key].text.length > 0) {
overrideHighlighting[highlight_key].text = overrideHighlighting[highlight_key].text.map( (text) => {
return text.replaceAll("<em>", `<b class="result-highlight">`).replaceAll("</em>", "</b>")
});
}
});
solrResponseWithHighlighting.highlighting = overrideHighlighting;
// Calculate total number of results (needed for pagination)
const totalResults = solrResponseAsSearchResponse.response.numFound;
// Calculate total number of pages
const totalPages = Math.ceil(totalResults / pageSize);
// Build breadcrumbs
let breadcrumbs: Breadcrumb[] = [
{ title: `${config.siteName}`, url: '/' },
{ title: `Search Results for ${qQuery}`, url: req.url }
];
// Render ejs page to output
res.render('search-results', {
breadcrumbs,
h: helpers,
query: qQuery,
page,
pageSize,
solrQuery: solrQuery,
totalResults,
totalPages,
...solrResponse
});
// res.render('search-error', { h: helpers, query: sanitizedQuery, error: { code: 400, message: 'Search query is required.'} });
})
.catch(error => {
if (typeof error === 'object' && error instanceof Error) {
// check for error from throw new Error(`Request HTTP error ${response.statusCode}: ${text}`) in solr.ts from
// solr-node-client dependency
const detectRequestHttpErrorRegExLit = /^Request HTTP error (?<statusCode>\d{1,3}): (?<text>\{.*\}$)/s;
const detectRequestHttpErrorRegExp = new RegExp(detectRequestHttpErrorRegExLit);
const matchRequestHttpErrorRegExpInError = error.message.match(detectRequestHttpErrorRegExp);
const statusCode = (matchRequestHttpErrorRegExpInError && matchRequestHttpErrorRegExpInError.groups && matchRequestHttpErrorRegExpInError.groups.statusCode);
const text = (matchRequestHttpErrorRegExpInError && matchRequestHttpErrorRegExpInError.groups && matchRequestHttpErrorRegExpInError.groups.text);
if (text) {
let solrRequestHttpInternalError = JSON.parse(text);
error = {
message: "Solr Client Request HTTP Error",
code: statusCode,
innerError: solrRequestHttpInternalError
};
}
else {
error = {
message: error
};
}
}
// Build breadcrumbs
const breadcrumbs: Breadcrumb[] = [
{ title: `${config.siteName}`, url: '/' },
{ title: `Search Error` + (qQuery ? ` for ${qQuery}` : ``), url: req.url }
];
// Render ejs page to output
res.render('search-error', { breadcrumbs, error, h: helpers, query: qQuery});
});
}
// // Sanitize search query to prevent code injection
// try {
// // Validate search query
// if (!query) {
// //return res.status(400).json({ error: 'q parameter is required' });
//
// }
// else {
// // Send search query to Solr
// const response = await axios.get(solrUrl + '/select', {
// params: {
// q: `text:${sanitizedQuery}`, // Query string with field name
// hl: 'true',
// 'hl.method': 'unified',
// 'hl.fl': '*',
// 'hl.snippets': 5,
// 'hl.tag.pre': '<strong class=\"result-highlight\">',
// 'hl.tag.post': '</strong>',
// 'hl.usePhraseHighlighter': true,
// start, // Start offset for pagination
// rows: 10, // Number of rows to return
// wt: 'json', // Response format (JSON)
// },
// });
//
// // Extract search results from Solr response
// const searchResults = response.data.response.docs;
// const highlightedSnippets = response.data.highlighting;
// // Calculate total number of results (needed for pagination)
// const totalResults = response.data.response.numFound;
// // Calculate total number of pages
// const totalPages = Math.ceil(totalResults / pageSize);
// // Send search results as JSON response
// //res.json('search-results', { query, searchResults, highlightedSnippets, page, pageSize, totalResults, totalPages });
// res.render('search-results', { h: helpers, query: sanitizedQuery, searchResults, highlightedSnippets, page, pageSize, totalResults, totalPages });
// }
// } catch (error) {
// // console.error('Error searching Solr:', error.message);
// // res.status(500).json({ error: 'Internal server error' });
// res.render('search-error', { h: helpers, query: sanitizedQuery, error });
// }
});
return searchRouter;
};