forked from nm3clol/nm3clol-express-app
189 lines
8.7 KiB
TypeScript
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;
|
|
}; |