Compare commits

2 Commits
Author SHA1 Message Date
ztimson 0a7e87f6b7 Patched search results
Publish Library / Build NPM Project (push) Successful in 3m16s
Publish Library / Tag Version (push) Successful in 19s
2026-09-20 20:29:28 -04:00
ztimson e12c3e3cc6 Fixed typo
Publish Library / Build NPM Project (push) Successful in 4m39s
Publish Library / Tag Version (push) Successful in 8s
2026-09-20 19:22:10 -04:00
4 changed files with 19 additions and 26 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@ztimson/zim-utils", "name": "@ztimson/zim-utils",
"version": "0.4.0", "version": "0.4.2",
"description": "Native, dependency-light ZIM archive reader/searcher and Kiwix catalog downloader for Node.js", "description": "Native, dependency-light ZIM archive reader/searcher and Kiwix catalog downloader for Node.js",
"author": "Zak Timson", "author": "Zak Timson",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -107,7 +107,7 @@ export class ZimManager {
const file = match.href + (match.href.endsWith('.zim') ? '' : '.zim'); const file = match.href + (match.href.endsWith('.zim') ? '' : '.zim');
await fs.promises.rm(path.join(this.#dir, file), {force: true}); await fs.promises.rm(path.join(this.#dir, file), {force: true});
await this.#ensureServer(); await this.#ensureServer();
await this.#waitUntilVicsible(file, {expect: false}); await this.#waitUntilVisible(file, {expect: false});
return {file: match.file, status: 'deleted'}; return {file: match.file, status: 'deleted'};
} }
+1 -1
View File
@@ -101,7 +101,7 @@ export function rerank(hits, termList) {
const title = fieldScore(hit.title, termList, {fuzzy: true}); const title = fieldScore(hit.title, termList, {fuzzy: true});
const summary = fieldScore(hit.summary, termList); const summary = fieldScore(hit.summary, termList);
const titleLower = (hit.title || '').toLowerCase(); const titleLower = (hit.title || '').toLowerCase();
const summaryLower = (hit.summary || '').toLowerCase(); const summaryLower = (hit.summary?._text || '').toLowerCase();
const phrase = termList.join(' ').toLowerCase(); const phrase = termList.join(' ').toLowerCase();
const titleExactPhrase = titleLower.includes(phrase) ? 1 : 0; const titleExactPhrase = titleLower.includes(phrase) ? 1 : 0;
+16 -23
View File
@@ -212,7 +212,7 @@ export class KiwixServer {
return makeArray(entries?.library?.book || []).map(e => { return makeArray(entries?.library?.book || []).map(e => {
const tags = e.tags.split(';'); const tags = e.tags.split(';');
const name = e.path.replace('.zim', ''); const name = e.path.replace(/\.zim$/i, '');
return { return {
id: e.id, id: e.id,
@@ -262,53 +262,43 @@ export class KiwixServer {
} }
async #rawSearch(termList, scoped, bookMap, limit) { async #rawSearch(termList, scoped, bookMap, limit) {
const groups = new Map(); const perBook = await Promise.all(scoped.map(async book => {
for(const book of scoped) {
const lang = book.language || '';
(groups.get(lang) ?? groups.set(lang, []).get(lang)).push(book);
}
const perGroup = await Promise.all([...groups.values()].map(async group => {
const params = new URLSearchParams({ const params = new URLSearchParams({
pattern: termList.join(' '), pattern: termList.join(' '),
format: 'xml', format: 'xml',
pageLength: String(limit), pageLength: String(limit),
'books.name': book.href,
}); });
for(const book of group)
params.append('books.name', book.name);
const res = await fetch(`${this.baseUrl}/search?${params}`); const res = await fetch(`${this.baseUrl}/search?${params}`);
if(!res.ok) return []; if(!res.ok) return [];
const found = fromXml(await res.text())?.rss?.channel?.item || []; const found = fromXml(await res.text())?.rss?.channel?.item || [];
return found.map(hit => { return found.map(hit => {
const book = bookMap.get(hit.book.title); const resultBook = bookMap.get(hit.book?.title) || book;
if(!book) return null;
const prefix = `/content/${book.href}/`; const prefix = `/content/${resultBook.href}/`;
const page = hit.link.startsWith(prefix) const page = hit.link.startsWith(prefix)
? hit.link.slice(prefix.length) ? hit.link.slice(prefix.length)
: hit.link.replace(/^\/+/, ''); : hit.link.replace(/^\/+/, '');
return { return {
id: book.id, id: resultBook.id,
title: hit.title, title: hit.title,
page, page,
name: book.name, name: resultBook.name,
publisher: book.publisher, publisher: resultBook.publisher,
href: `${book.href}/${page}`, href: `${resultBook.href}/${page}`,
icon: book.icon, icon: resultBook.icon,
viewer: this.baseUrl + hit.link, viewer: this.baseUrl + hit.link,
summary: hit.description, summary: hit.description,
xapianScore: +hit.score || 0, xapianScore: +hit.score || 0,
}; };
}).filter(Boolean).sort((a, b) => b.xapianScore - a.xapianScore); }).filter(Boolean);
})); }));
return rrfMerge(perGroup); return rrfMerge(perBook);
} }
/** Fulltext search across local ZIMs with ranking, diversification and spelling correction. */ /** Fulltext search across local ZIMs with ranking, diversification and spelling correction. */
@@ -320,6 +310,7 @@ export class KiwixServer {
const books = await this.list(); const books = await this.list();
const bookMap = new Map(books.map(b => [b.title, b])); const bookMap = new Map(books.map(b => [b.title, b]));
const scoped = sources?.length const scoped = sources?.length
? books.filter(b => sources.includes(b.name) || sources.includes(b.href)) ? books.filter(b => sources.includes(b.name) || sources.includes(b.href))
: books; : books;
@@ -360,8 +351,10 @@ export class KiwixServer {
} }
} }
const ranked = rerank(hits, activeTerms);
return { return {
results: diversify(rerank(hits, activeTerms), limit), results: diversify(ranked, limit),
spellcheck, spellcheck,
}; };
} }