- Duplicates tab: groups are confirmed by each file's last 8 KiB and keyed by head+tail; unchecked files are never shown. False-positive duplicates drop from 3.4% to 0.1%, and indexing is unchanged. - Encrypted search cache cap raised to 192 MiB at 150 B per file: a keystroke at 1M files takes 59 ms, down from 99 ms. - Case-insensitive filename matching uses a memchr2 scan (1.18x). - Benches run again on the compact index, seed with the indexer's 2 GiB WAL cap (peak scratch 6.2 GB -> 2.45 GB), and every recorded figure is re-measured. - A fresh index no longer logs a rebuild warning. - CI: the sandbox memory test's allocation survives release builds, and --toggle retries an interrupted read.
133 lines
7.3 KiB
Markdown
133 lines
7.3 KiB
Markdown
# QuickSearch design notes
|
|
|
|
A brief record of the design choices and the reasoning behind them. The code
|
|
and its comments specify exact behavior.
|
|
|
|
## Shape of the codebase
|
|
|
|
`quicksearch-core` is the library (indexing, storage, search), and
|
|
`quicksearch-gui` is the app (an immediate-mode egui desktop). On Linux the
|
|
app binary also searches from a terminal; on Windows the separate
|
|
`quicksearch-cli` does. The backend is synchronous Rust: plain threads and
|
|
mpsc channels, no async runtime, because a small, fixed set of long-lived
|
|
workers is simpler as threads.
|
|
|
|
## Optimization
|
|
|
|
Performance is measured, not guessed: benchmarks and probes measure indexing
|
|
and search speed, memory churn, peak memory and disk I/O.
|
|
|
|
## Storage
|
|
|
|
The index is a single SQLite database: file metadata, an FTS5 full-text index
|
|
over document contents, and the extracted text itself, stored zstd-compressed
|
|
for snippets, ranking and verification. The database runs in WAL mode with exactly one
|
|
writer at a time, so read-only searches are never blocked by indexing. Schema
|
|
changes wipe and rebuild the index rather than migrating: the index is a cache
|
|
of the filesystem, so rebuilding is always safe, and only the indexer may do
|
|
it. Nothing deletes the file at the configured database path unless it is
|
|
empty, recognisably an index, or indistinguishable from an encrypted one, and a
|
|
file another program holds open is never deleted.
|
|
FTS5 runs in trigram mode for full-text search and also speeds up regex
|
|
and fuzzy searches. By default it records no word positions: every candidate
|
|
is checked against the stored text anyway, so positions only narrow the
|
|
candidates, and dropping them makes the index several times smaller and
|
|
faster to build, for content searches up to two or three times slower. A
|
|
setting trades the other way. Tuning this database is central to QuickSearch's
|
|
performance.
|
|
|
|
## The search path never stalls
|
|
|
|
Searches answer from the index and never wait on the filesystem or the
|
|
indexer. Each keystroke cancels the previous query. Results stream in strongest
|
|
pass first; within a pass they arrive as found, so a better match found later
|
|
can land above weaker ones. A small watcher then checks the visible rows
|
|
against the disk and hands changed files to the indexer; a vanished row is
|
|
shown as gone, and only the index's file watcher or the next run removes it.
|
|
|
|
## Indexing
|
|
|
|
Each indexed root gets its own directory walker and content-extraction pool,
|
|
but all database writes go through one thread and one connection. That writer
|
|
serves the walkers first and slices extraction work, so one root's large
|
|
documents never stall another root's progress. Changing what is indexed does
|
|
not throw the index away: the configuration difference becomes a plan to
|
|
delete what fell out of scope and walk what is newly covered, applied while
|
|
searching and indexing keep running. A root that cannot be reached, or that
|
|
lists as empty because nothing is mounted on it, never counts as removed or
|
|
emptied; its entries wait for it to come back. The app parses each PDF,
|
|
audio file and legacy Office document (doc, xls, ppt) in a child process
|
|
under CPU and memory limits, so a hostile file that would overflow a
|
|
third-party parser's stack or allocate without bound fails alone.
|
|
|
|
## Freshness
|
|
|
|
Filesystem watchers turn changes into small incremental updates. When roots
|
|
are too large for watchers to scale, a full reindex on a configurable interval
|
|
is the backstop. There is no background daemon and no "start with your
|
|
session" option: QuickSearch starts and indexes in moments, and a search tool
|
|
has no business running when it is not being used.
|
|
|
|
## Security
|
|
|
|
QuickSearch has no more permissions than any other user program, but by
|
|
gathering what it can read into one searchable place it concentrates the risk
|
|
of data theft. So the index can be encrypted on disk with SQLCipher, keyed
|
|
from a password through Argon2id; the OS keychain can remember the key.
|
|
Encryption is for confidentiality at rest: a stolen disk, a synced backup or a
|
|
copied index reveals neither file names nor contents, and also prevents
|
|
non-authorized users or apps from having a faster way to find sensitive files
|
|
than scanning the originals. It does not protect against software running as the
|
|
user, which could read the original files anyway. SQLCipher's per-page HMAC, which
|
|
only detects tampering, is off because tampering with a search tool is low-threat
|
|
and it costs significant search performance.
|
|
|
|
Turning protection on does not securely erase the unencrypted index it replaces.
|
|
The file is simply deleted and parts can remain recoverable in freed disk space.
|
|
Changing the password leaves the old encrypted index behind the same way, still
|
|
readable with the old password.
|
|
|
|
## Deliberate omissions
|
|
|
|
- Files whose names are not valid UTF-8 are silently skipped. A lossily
|
|
converted path must never become a database key, and the corner case does
|
|
not justify UI.
|
|
- Symbolic links and junctions inside an indexed folder are not followed. To
|
|
index what one points at, add the target as its own folder; a folder chosen
|
|
as a root may itself be a link.
|
|
- A file counts as unchanged while its size and its modified time, to the
|
|
whole second, match the index. A same-size rewrite within one second, or a
|
|
same-size replacement that keeps the old time, is missed until the file
|
|
changes again. Sub-second times would cost a rebuild and gain nothing on
|
|
FAT, which keeps two-second times, and the size already catches appends
|
|
and most replacements.
|
|
- Duplicate groups are a strong suspicion, not a certainty: indexing hashes
|
|
only each file's size and head, which the walk reads anyway to detect the
|
|
file's type, so it costs little. Only for the groups it lists, the
|
|
Duplicates tab also reads each member's tail, and a listed group is the
|
|
files sharing both head and tail, so the extra reads are paid where
|
|
precision matters. On 86k real files at 8 KiB, the head alone misgroups
|
|
3.4% of them and head and tail 0.1%. Nothing is listed before its tail is
|
|
checked: a file whose tail cannot be read is left out, and so is a group
|
|
it leaves with one file. A hidden group is remembered by head and tail. The
|
|
Duplicates tab compares files byte for byte on request and never deletes or
|
|
modifies them; it tells you where to look. Hard links and bind-mounted
|
|
copies of one file group as copies, and deleting one frees nothing;
|
|
verification calls them identical, which they are.
|
|
- Only one instance may open an index for writing where the filesystem
|
|
supports locks. A second launch raises the running window instead, and
|
|
refuses if it cannot rather than risk corruption; where the filesystem
|
|
cannot lock, it raises an instance that answers and otherwise starts
|
|
anyway. The guard is a kernel lock, so a crash never leaves you locked out.
|
|
- The app bundles one text face and one monospace face, nothing more. CJK,
|
|
Arabic, Hebrew, Thai, Indic scripts and emoji come from the system's own
|
|
fonts, found after the first frame, and show as boxes where it has none.
|
|
egui does no shaping or right-to-left layout, so Arabic letters show
|
|
unjoined, Thai and Indic marks sit out of place, and Arabic and Hebrew run
|
|
left to right.
|
|
|
|
## Where the detail lives
|
|
|
|
Tests live in each crate; benchmarks and profiling probes in quicksearch-core
|
|
(`cargo bench`, `examples/`). Release and CI mechanics are
|
|
documented in `.forgejo/workflows/ci.yml` and the scripts under `packaging/`.
|