Skip to content

Commit

Permalink
[search] order non-main index entries after other results (#11696)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradking committed Mar 18, 2024
1 parent ff25286 commit a9383a0
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ Bugs fixed
* #12052: Remove ``<script>`` and ``<style>`` tags from the content of search result
summary snippets.
Patch by James Addison.
* #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
72 changes: 46 additions & 26 deletions sphinx/themes/basic/static/searchtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ const _displayNextItem = (
// search finished, update title and status message
else _finishSearch(resultCount);
};
// Helper function used by query() to order search results.
// Each input is an array of [docname, title, anchor, descr, score, filename].
// Order the results by score (in opposite order of appearance, since the
// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically.
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 @@ -297,16 +313,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].
const normalResults = [];
const nonMainIndexResults = [];

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

const queryLower = query.toLowerCase().trim();
for (const [title, foundTitles] of Object.entries(allTitles)) {
if (title.toLowerCase().trim().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 @@ -321,46 +340,47 @@ 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)));

// 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;
});
if (Scorer.score) {
normalResults.forEach((item) => (item[4] = Scorer.score(item)));
nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item)));
}

// Sort each group of results by score and then alphabetically by name.
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 = [...nonMainIndexResults, ...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 a9383a0

Please sign in to comment.