Skip to content

Commit

Permalink
search: order non-main index entries after other results
Browse files Browse the repository at this point in the history
Since commit 8ae8183 (Support searching for index entries (sphinx-doc#10819),
2022-09-20, v5.2.0~16), index entries are returned as search results.
When the query string exactly matches an indexed term, all index
entries become search results with score 100.  This places them above
most other search results.

Reporting "main" index entries early in the search results makes sense,
but non-main index entries are often just arbitrary cross-references to
the indexed term.  Collect them in a separate group that is always
placed after other results.  This avoids obscuring the main entries and
other more-important matches such as document titles.

Fixes: sphinx-doc#11578
  • Loading branch information
bradking committed Sep 28, 2023
1 parent b9c8598 commit 703aa9b
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Bugs fixed
* #11483: singlehtml builder: Fix MathJax lazy loading when the index does not
contain any math equations.
Patch by Bénédikt Tran.
* #11578: HTML Search: Order non-main index entries after other results.
Patch by Brad King.

Testing
-------
Expand Down
4 changes: 2 additions & 2 deletions sphinx/search/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,10 @@ def freeze(self) -> dict[str, Any]:
for title, titleid in titlelist:
alltitles.setdefault(title, []).append((fn2index[docname], titleid))

index_entries: dict[str, list[tuple[int, str]]] = {}
index_entries: dict[str, list[tuple[int, str, bool]]] = {}
for docname, entries in self._index_entries.items():
for entry, entry_id, main_entry in entries:
index_entries.setdefault(entry.lower(), []).append((fn2index[docname], entry_id))
index_entries.setdefault(entry.lower(), []).append((fn2index[docname], entry_id, main_entry == "main"))

return dict(docnames=docnames, filenames=filenames, titles=titles, terms=terms,
objects=objects, objtypes=objtypes, objnames=objnames,
Expand Down
64 changes: 42 additions & 22 deletions sphinx/themes/basic/static/searchtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,18 @@ const _displayNextItem = (
// search finished, update title and status message
else _finishSearch(resultCount);
};
const _orderResultsByScoreThenName = (a, b) => {
const leftScore = a[4];
const rightScore = b[4];
if (leftScore === rightScore) {
// same score: sort alphabetically
const leftTitle = a[1].toLowerCase();
const rightTitle = b[1].toLowerCase();
if (leftTitle === rightTitle) return 0;
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
}
return leftScore > rightScore ? 1 : -1;
};

/**
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
Expand Down Expand Up @@ -284,16 +296,19 @@ const Search = {
// console.info("required: ", [...searchTerms]);
// console.info("excluded: ", [...excludedTerms]);

// array of [docname, title, anchor, descr, score, filename]
let results = [];
// Collect multiple result groups to be sorted separately and then ordered.
// Each is an array of [docname, title, anchor, descr, score, filename].
let normalResults = [];
let nonMainIndexResults = [];

_removeChildren(document.getElementById("search-progress"));

const queryLower = query.toLowerCase();
for (const [title, foundTitles] of Object.entries(allTitles)) {
if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) {
for (const [file, id] of foundTitles) {
let score = Math.round(100 * queryLower.length / title.length)
results.push([
normalResults.push([
docNames[file],
titles[file] !== title ? `${titles[file]} > ${title}` : title,
id !== null ? "#" + id : "",
Expand All @@ -308,46 +323,51 @@ const Search = {
// search for explicit entries in index directives
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
for (const [file, id] of foundEntries) {
let score = Math.round(100 * queryLower.length / entry.length)
results.push([
for (const [file, id, isMain] of foundEntries) {
const score = Math.round(100 * queryLower.length / entry.length);
const result = [
docNames[file],
titles[file],
id ? "#" + id : "",
null,
score,
filenames[file],
]);
];
if (isMain) {
normalResults.push(result);
} else {
nonMainIndexResults.push(result);
}
}
}
}

// lookup as object
objectTerms.forEach((term) =>
results.push(...Search.performObjectSearch(term, objectTerms))
normalResults.push(...Search.performObjectSearch(term, objectTerms))
);

// lookup as search terms in fulltext
results.push(...Search.performTermsSearch(searchTerms, excludedTerms));
normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms));

// let the scorer override scores with a custom scoring function
if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item)));
if (Scorer.score) {
normalResults.forEach((item) => (item[4] = Scorer.score(item)));
nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
}

// now sort the results by score (in opposite order of appearance, since the
// display function below uses pop() to retrieve items) and then
// alphabetically
results.sort((a, b) => {
const leftScore = a[4];
const rightScore = b[4];
if (leftScore === rightScore) {
// same score: sort alphabetically
const leftTitle = a[1].toLowerCase();
const rightTitle = b[1].toLowerCase();
if (leftTitle === rightTitle) return 0;
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
}
return leftScore > rightScore ? 1 : -1;
});
normalResults.sort(_orderResultsByScoreThenName);
nonMainIndexResults.sort(_orderResultsByScoreThenName);

// Combine the result groups in (reverse) order.
// Non-main index entries are typically arbitrary cross-references,
// so display them after other results.
let results = [];
results.push(...nonMainIndexResults);
results.push(...normalResults);

// remove duplicate search results
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
Expand Down

0 comments on commit 703aa9b

Please sign in to comment.