Indexing re-enqueues every eligible item on each start to find what
changed, and checked each one's stored hash with its own query, so a
large library ran thousands of queries at startup.
Replace the bundled transformers.js/ONNX Runtime worker with a Zotero.ML
engine on the native ONNX backend, which embeds a batch of abstracts
several times faster and runs the model outside the main process. The
runtime downloads the model files and caches them in the profile
directory, so drop the model directory in the data directory along with
the code that filled it.
Size batches by the amount of text they hold rather than by a fixed item
count, since a batch of long abstracts needs far more memory than the
same number of short ones, and shrink the budget further while the
system is under memory pressure.
The runtime runs models in a separate, memory-gated inference process
using the native ONNX Runtime and llama.cpp libraries Firefox ships. It
reads its model configuration, runtime configuration, and model-host
policy from Remote Settings, which our build omits, so loading the empty
module throws on the first collection lookup. Supply those collections
directly instead.
Allow only the two backends that use those native libraries, onnx-native
and llama.cpp. The rest either load a WebAssembly runtime from a Remote
Settings attachment, whose record lookup also needs the Translations
actors our build omits (onnx, wllama, and the best-* backends that fall
back to them), or aren't local at all (openai).
A best-match rerank re-ran the underlying search on every embeddings
update, even though only the ranking depends on the embeddings. Reuse
the cached search results and recompute just the ranking, except when a
top-K cutoff makes membership depend on the scores.
The item tree reran the active best-match search on every 'refresh' item
event, so unrelated bursts (e.g., full-text indexing) triggered a full
re-search and re-score. Flag the embeddings indexer's own notifications
and rerank only for those.
Changing the mode -- including disabling -- silently dropped the
stored embeddings and any other downloaded model, costing a full
reindex. Prompt first, since the menu click gives no hint of the cost.
While a best-match search runs against a partially built embeddings
index, results cover only the indexed items and can look arbitrary.
Show the indexing progress in a banner above the items list, updating
as the index fills, so incomplete results aren't mistaken for a
complete ranking.
The query is trimmed and a single pair of wrapping quotes is stripped
-- they carry no phrase semantics, since the whole query embeds as one
string. A query that normalizes to nothing (e.g., just quotes) is
treated as no search at all rather than being scored against noise.
A requested stop only takes effect between batches, so the Stop button
looked unresponsive. Report the stop request in the status line and
disable the button until the batch finishes.
Covers saved-search ranking and quick-search precedence, mixed
top-K/collection selections, rank-only behavior with an unavailable
index, reranking on indexer refresh events, scoped 'Match any' saves
keeping the marker at the root, numeric-operator round trips, and
concurrent query embeds sharing one worker call.
A root-level 'bestMatch' condition -- serialized as a marker like
joinMode and resultLevel -- ranks results with the Relevance column via
a "Sort results by best match for" field below the root group's
conditions, composing semantic ranking with Boolean filtering. A
best-match quick search converts to it via the Advanced Search button,
and a selected saved search's own marker activates ranking too,
overridden by an active best-match quick search.
Without a cutoff the condition is rank-only and membership is untouched
-- including while the index is unavailable -- so a saved search acts
identically as a source, and unscoreable items just sort last. With the
optional "keeping top" cutoff, carried in the marker's operator,
membership becomes the K most similar results, applied in search() so
scopes, counts, and the API see the same set. A transient search's
cutoff, which applies uniformly to every selected row, is reapplied
over the merged results so a multi-collection selection returns K
members total; a saved search's cutoff is part of its own membership
and never trims other selected rows. The query embedding is cached
in-flight, so the per-row membership passes and the merged ranking
share one worker embed.
Rows kept identifying the active mode as bestMatch after the model was
disabled in the preferences, leaving the active filter returning nothing.
Observe the model pref, switch to Fields & Tags, and rerun any active
search.
download() skipped every existing file, so after a revision bump the old
files were kept and then marked as the new revision. Stamp the directory
with the revision being downloaded and clear it on mismatch, so bumps
replace the files while interrupted downloads of the same revision still
resume.
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.
New, changed, or removed vectors now rerank an active best-match
search: the indexer announces committed batches -- and index clears from
disabling or a model switch -- with a coalesced 'refresh' item event,
and the items view reruns the search for it. Batch writes also skip
items deleted while the batch was embedding, so a delete can't be undone
by an in-flight batch, and the delete notifier is awaited.
A search could embed the query with one model's prefix on a worker
initialized for another and compare it against vectors from a third.
Tag the worker with the model version it was initialized for, make
scoring wait out an in-progress model switch, refuse to score an index
that wasn't stamped by the active model, and discard results if the
model changes mid-scoring. The items view treats a not-ready index as
an empty result rather than showing an unranked scope.
Semantic similarity has no natural relevance threshold, so instead of
asking the user to pick an arbitrary result count, show every scored
item and surface the ranking directly: a Relevance column appears and
becomes the sort while a best-match search is active, and the previous
sort and columns return when it clears. The merged results are scored
in a single pass in the row provider, so ranks are global across a
multi-collection selection, child items (attachments, notes,
annotations) rank via their top-level item, and equal scores get equal
ranks that order deterministically via the secondary sort fields. Items
without a stored embedding are filtered out.
Each cell renders the score's position within the model's display range
as a bar, so relevant results read as full and the irrelevant tail
reads as empty. The ranges are provisional per-model display constants.
Sorting uses the ranks, which are also exposed to assistive technology
and as the cell tooltip. On a focused selected row the bar
switches to white so the fill doesn't vanish into the accent selection
background.
The embeddings are a local, rebuildable, model-specific index, so they
don't belong in the main database or its backups. Follow the full-text
content index pattern: a lazily attached embeddings.sqlite versioned via
PRAGMA user_version, tied to the main database by localUserKey, with
corruption recovery and idle-maintenance vacuuming via the DBConnection
hooks. Since a cross-database foreign key isn't possible, item deletions
now clear embeddings via the notifier, and the indexed-model identity
moves from a pref into the database's meta table.
"bestMatch" names what the condition does -- rank results by how well
they match the text -- rather than the current scoring mechanism, so
the name can account for later changes to how it works. Rename the
condition-facing identifiers with it; the embeddings engine keeps its
similarity vocabulary.
- added environment to run embedding models locally
(transformers.js, ONNX Runtime WASM binary, etc.). The actual
inference execution happens in a separate worker environment (worker.js)
- added local itemEmbeddings table to store embeddings locally
- in advanced preferences, one can select two options for
semantic search model: english and multilingual. English model
(bge-small-en-v1.5) is better for english-only corpus
but multilingual (multilingual-e5-small) is necessary to handle
abstracts with any other language than english. We can add
more language-specific models as needed.
- when the model is selected, Zotero.Embeddings.download
will download the model (quantized ~100mb) and store it locally.
- Zotero.Embeddings.Indexing will start a process to
index all regular items with title+abstract. It happens in batches
and takes some time. The progress will appear in the
advanced preferences pane. Embeddings are inserted
into itemEmbeddings SQL table. For now, the table is local
only, no syncing is involved.
- when embedding model pref is set to "Disabled", the model
is deleted and embeddings table is cleared.
- when an embedding model is selected, quick search dropdown
has a new "Similarity" mode, which will run semantic search
on the current scope of items.
- semantic search does not clearly define what counts
as "relevant" and what is "not relevant". In addition,
it will change depending on the library and query. So
we cannot semantically filter out items the way
it is done via SQL. Semantic search returns the ranking
but items cannot be sorted because it is done by the itemTree
based on column selection.
So in "similarity" quicksearch mode, there is also a dropdown
to select how many top relevant items to keep (top 5 - top 100).
It allows the user to keep the most relevant items depending
on the context, without conflicting with itemTree sorting.
- semantic search happens in-memory. On a large 5K library
it's fast, but we could consider sqlite-vec extension if
needed.
The menu labeled Annotation Type listed "Highlight annotation" and "Image
Annotation", from the strings the reader announces annotations with. Use
the short names, which existed for two of the six.
mozinference (which is currently CPU-only) is better for many tasks, but
plugins may want WebGPU for some features (e.g., chatbots), so just
follow Firefox, which currently enables it by default for Windows and
Apple Silicon macOS. Adds 5.8 MB compressed to the Windows installer.
Binding is meaningful for a condition that matches at every level -- a
tag bound to an attachment means the tag is on the attachment -- but a
group carrying one lost the binding as soon as the search was
serialized, so "items with an attachment tagged foo" couldn't be built.
The row label builder skipped values that weren't strings, so a numeric
cell -- a note count, or a pre-1000 year (which is parsed with
parseInt()) -- announced only its column
name.
Also switch to formatColumnName() while we're here, so row labels
resolve the column name the same way the header does.
The guard tested the newly clicked column's settings entry while the
delete targeted the previously sorted column's, so the old direction
survived when the new column had no entry, and clicking threw when the
old column had none.
Skip crashhelper (all platforms -- the crash reporter is disabled) and
desktop-launcher and nmhproxy.exe (Windows).
Also skip dxcompiler.dll (Windows, 22 MB), which is loaded only by
WebGPU, now also disabled via pref. Nothing renders with WebGPU, and
local ML -- including any future plugin API -- should go through
Firefox's inference libraries (mozinference/onnxruntime), which are
compiled CPU-only.
XULButtonElement only opens a type=menu button's menu when the button
is the event's original target, which worked before because xul.css
gave .toolbarbutton-menu-dropmarker 'pointer-events: none'. That rule
was removed along with the dropmarker element, so clicks on our
recreated dropmarker did nothing.
The menu icon element is now an <html:img> that renders the 'image'
attribute or 'content: var(--menuitem-icon)', and 'list-style-image' is
ignored, so iconic menuitems showed broken-image placeholders on
Windows and Linux. list-style-image is kept for native macOS menus,
which still read it.
Firefox 153 removed the chrome://browser/skin/window-controls/ SVGs and
now draws the caption buttons with Segoe Fluent Icons glyphs (falling
back to Segoe MDL2 Assets on Windows 10), which broke the Windows build.
Port the new browser.css button styling and stop keeping the removed
directory in build.sh.
An exact-match condition compared with SQLite's case-sensitive '=' and
skipped the normalized shadow columns, so 'publication is "review of
finance"' missed "Review of Finance" while every other kind of search
matched it.
Better fix than d5aacafe3f -- we don't need source-repo info in our
builds at all, so just disable MOZ_INCLUDE_SOURCE_INFO rather than
synthesizing it.
Firefox's official-build source-repo.h generator only auto-detects
Mercurial, so building from our git checkout left the file empty and
packaging failed with "no preprocessor directives found". Derive
MOZ_SOURCE_REPO and MOZ_SOURCE_CHANGESET from git in the mozconfig so
configure records them for any invocation, including a manual
./mach package.
app/win/build added the cross-compilation target to whatever toolchain
was default before switching to RUST_VERSION, so after a Rust version
bump the new toolchain lacked the aarch64/i686 target and the arm and
win32 builds failed at configure. Set the default first.
Bug 1974578 added oncommand to GlobalEventHandlers for the Invoker
Commands API, so the assignment in _initImportedNodesPostInsert() -- an
inert expando write until now -- started setting the WebIDL event
handler, and [LegacyTreatNonObjectAsNull] turns a string into null. That
wiped the handlers compiled when the pane fragment was imported, leaving
every inline oncommand handler in the preferences panes dead.
importNode() compiles the attribute handlers itself, so the loop can
just be removed.
Bug 1980856 removed both wantdropmarker support and the rule that gave
.toolbarbutton-menu-dropmarker its image, once Firefox's last consumer
stopped using it, so the New Item, Add Attachment, and New Note buttons
lost their chevrons, along with the ones in advanced search, the feed
settings dialog, and Scaffold.
Draw it with a background, since a <dropmarker> doesn't render
list-style-image, and use chevron-6 to match the quick search dropmarker
and the menu marker on our own buttons. The rule also moves out of the
toolbarbutton block, since a plain <button> can host one too.
Firefox now creates a clear button for <input type="search"> in chrome
documents regardless of layout.forms.input-type-search.enabled (bug
1655503), which doubled up with the one we draw ourselves.