mirror of
https://github.com/zotero/zotero.git
synced 2026-10-07 02:58:09 +00:00
Stop best-match scoring for superseded queries
Refreshes are serialized, so with scoring slower than the quick-search debounce, intermediate queries queued instead of becoming obsolete. Bump a generation counter when a filter or the selected rows change and check it between scoring chunks, abandoning the stale pass and leaving the rows for the newer refresh to replace.
This commit is contained in:
parent
132820a374
commit
356d36e4aa
2 changed files with 47 additions and 3 deletions
|
|
@ -98,6 +98,9 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider {
|
|||
constructor(itemTree) {
|
||||
super(itemTree);
|
||||
this.collectionTreeRows = [];
|
||||
// Bumped when a filter changes, so an in-flight best-match scoring
|
||||
// pass for a superseded query can stop (see _applyBestMatch())
|
||||
this._bestMatchGeneration = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -195,10 +198,18 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider {
|
|||
}
|
||||
}
|
||||
let scores;
|
||||
let generation = this._bestMatchGeneration;
|
||||
try {
|
||||
scores = await Zotero.Embeddings.scoreItemIDs(query, [...new Set(sourceIDByItem.values())]);
|
||||
scores = await Zotero.Embeddings.scoreItemIDs(query, [...new Set(sourceIDByItem.values())], {
|
||||
// A newer filter (e.g. more typed search text) makes this
|
||||
// query obsolete -- stop scoring and let its refresh take over
|
||||
shouldCancel: () => generation !== this._bestMatchGeneration
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Zotero.Embeddings.ScoringCancelledError) {
|
||||
throw e;
|
||||
}
|
||||
// Scoring can fail while the model is still downloading or the
|
||||
// index is being rebuilt -- show no results rather than an
|
||||
// unranked scope
|
||||
|
|
@ -374,6 +385,8 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider {
|
|||
let resetColumns = viewMode != this._viewMode;
|
||||
this._viewMode = viewMode;
|
||||
this.collectionTreeRows = collectionTreeRows;
|
||||
// Cancel any in-flight best-match scoring for the replaced selection
|
||||
this._bestMatchGeneration++;
|
||||
|
||||
// When the selection spans multiple libraries, group items by library in
|
||||
// collections-list order (the order of the selected rows), with a header
|
||||
|
|
@ -440,6 +453,7 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider {
|
|||
changed = changed || rowChanged;
|
||||
}
|
||||
if (changed) {
|
||||
this._bestMatchGeneration++;
|
||||
this._filterRefreshPromise = this.refresh({ restoreSelection: true });
|
||||
}
|
||||
// An unchanged filter can arrive while a previous filter's refresh is still in
|
||||
|
|
@ -529,7 +543,19 @@ class CollectionViewItemTreeRowProvider extends ItemTreeRowProvider {
|
|||
}
|
||||
// The semantic stage: one scoring pass over the merged results
|
||||
if (bestMatchSearch) {
|
||||
newSearchItems = await this._applyBestMatch(newSearchItems);
|
||||
try {
|
||||
newSearchItems = await this._applyBestMatch(newSearchItems);
|
||||
}
|
||||
catch (e) {
|
||||
// A newer filter superseded this one mid-scoring -- leave the
|
||||
// rows as they are and let the newer filter's refresh replace
|
||||
// them
|
||||
if (e instanceof Zotero.Embeddings.ScoringCancelledError) {
|
||||
deferred.resolve();
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
let newSearchItemIDs = new Set(newSearchItems.map(item => item.treeViewID));
|
||||
// In Recently Read, the search matches parent items, but the items that were
|
||||
|
|
|
|||
|
|
@ -476,6 +476,17 @@ Zotero.Embeddings = new function () {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Thrown when scoring is abandoned via the shouldCancel callback -- e.g.
|
||||
* because a newer query superseded the one being scored
|
||||
*/
|
||||
this.ScoringCancelledError = class extends Error {
|
||||
constructor(message = 'Scoring cancelled') {
|
||||
super(message);
|
||||
this.name = 'EmbeddingsScoringCancelledError';
|
||||
}
|
||||
};
|
||||
|
||||
function _ensureWorker() {
|
||||
if (_worker) {
|
||||
return;
|
||||
|
|
@ -717,9 +728,13 @@ Zotero.Embeddings = new function () {
|
|||
*
|
||||
* @param {String} queryText
|
||||
* @param {Number[]} itemIDs - Candidate item IDs to score
|
||||
* @param {Object} [options]
|
||||
* @param {Function} [options.shouldCancel] - Checked between chunks;
|
||||
* return true to abandon scoring with a ScoringCancelledError (e.g.
|
||||
* because a newer query made this one obsolete)
|
||||
* @return {Promise<Map>} - itemID -> similarity score (higher is more similar)
|
||||
*/
|
||||
this.scoreItemIDs = async function (queryText, itemIDs) {
|
||||
this.scoreItemIDs = async function (queryText, itemIDs, { shouldCancel } = {}) {
|
||||
let scores = new Map();
|
||||
if (!itemIDs.length || !this.isEnabled()) {
|
||||
return scores;
|
||||
|
|
@ -749,6 +764,9 @@ Zotero.Embeddings = new function () {
|
|||
// parameter limit for large collections), scoring each as we go.
|
||||
let chunkSize = 500;
|
||||
for (let i = 0; i < itemIDs.length; i += chunkSize) {
|
||||
if (shouldCancel && shouldCancel()) {
|
||||
throw new this.ScoringCancelledError();
|
||||
}
|
||||
// If the model changed while we were scoring, the scores computed
|
||||
// so far mix models -- discard them
|
||||
if (generation !== _modelGeneration) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue