Skip to content

feat(search): implement FTS5 w/ sqlite for faster and better searching#6839

Open
perfectra1n wants to merge 48 commits intomainfrom
feat/rice-searching-with-sqlite
Open

feat(search): implement FTS5 w/ sqlite for faster and better searching#6839
perfectra1n wants to merge 48 commits intomainfrom
feat/rice-searching-with-sqlite

Conversation

@perfectra1n
Copy link
Member

@perfectra1n perfectra1n commented Aug 30, 2025

Metric Value
Files Changed 109
Lines Added +15,575
Lines Removed -1,557
Net Change +14,018 lines
Commits 34
Migration File 0234__add_fts5_search.ts
New Test Files 16 spec files
New Indexes Created 18 performance indexes

Replaces the JavaScript-based fulltext search with native SQLite FTS5 (Full-Text Search 5) using trigram tokenization. This enables:

  • 50-100x faster substring matching compared to LIKE queries
  • Language-agnostic search (works for CJK, Cyrillic, Arabic, etc.)
  • Fuzzy matching with automatic fallback when exact matches are insufficient
  • Content snippets with highlighted matches in search results

Why It Was Implemented

The existing JavaScript-based search had significant performance issues:

  • Slow on large databases (10,000+ notes)
  • Required loading all note content into memory
  • No support for efficient substring matching
  • Poor handling of non-Latin scripts

Risk Assessment

Area Risk Level Notes
Database Migration Medium Creates virtual tables and triggers; includes rollback strategy
Protected Notes Low Explicitly excluded from FTS index; searched separately via decryption
Performance Low Extensive indexing; 18 new indexes with ANALYZE
Backwards Compatibility Low API unchanged; internal implementation only

Priority Review Files

  1. apps/server/src/migrations/0234__add_fts5_search.ts - Migration logic
  2. apps/server/src/services/search/fts/search_service.ts - Core FTS operations
  3. apps/server/src/services/search/expressions/note_content_fulltext.ts - Integration point
  4. apps/server/src/services/search/sqlite_functions.ts - Custom SQL functions

Breaking Changes

Database Migration Requirements

  • FTS5 Virtual Tables: Creates notes_fts and attributes_fts
  • 12 Database Triggers: Auto-sync FTS indexes with source tables
  • Legacy Table Cleanup: Removes note_search_content and note_tokens tables

API Changes

None - All changes are internal implementation details. The search API remains unchanged.

Backwards Compatibility

  • Existing search queries continue to work
  • Search operators (=, *=*, ~=, etc.) retain their behavior

Protected Notes Handling

Protected notes are explicitly excluded from the FTS index:

WHERE n.isProtected = 0  -- Skip protected notes

Protected notes are searched separately using decryption when a protected session is active. This ensures:

  • No unencrypted content in FTS index
  • Protected content never indexed to disk
  • Search only works when user has active protected session

Input Validation

  • Maximum token length: 500 characters
  • Maximum query tokens: Prevents excessively complex queries
  • LIKE wildcards escaped: Prevents unintended pattern matching

Performance Implications

Trigram Tokenization Benefits

FTS5 with trigram tokenization provides:

Query Type Old Performance New Performance Improvement
Substring match O(n) scan O(log n) index lookup 50-100x
Phrase search O(n) scan O(log n) index lookup 50-100x
Multiple tokens O(n*m) O(log n) per token Significant

New Indexes Created (18 Total)

Notes Table:

  • IDX_notes_search_composite - Common search filters
  • IDX_notes_metadata_covering - Note metadata queries
  • IDX_notes_protected_deleted - Protected notes filtering

Branches Table:

  • IDX_branches_tree_traversal - Hierarchy traversal
  • IDX_branches_covering - Branch queries
  • IDX_branches_note_parents - Reverse lookup

Attributes Table:

  • IDX_attributes_search_composite - Attribute searches
  • IDX_attributes_covering - Attribute queries
  • IDX_attributes_inheritable - Inherited attributes
  • IDX_attributes_labels - Label-specific
  • IDX_attributes_relations - Relation-specific

Other Tables:

  • IDX_blobs_content_size - Blob filtering
  • IDX_attachments_composite - Attachment queries
  • IDX_revisions_note_date - Revision queries
  • IDX_entity_changes_sync - Sync operations
  • IDX_recent_notes_date - Recent notes

Memory Considerations

  • FTS5 virtual tables use disk-backed indexes
  • detail='full' increases index size by ~50% but enables phrase queries
  • Indexes are maintained automatically via triggers
  • ANALYZE runs on all tables to optimize query planner

Migration Details

Migration File

apps/server/src/migrations/0234__add_fts5_search.ts

Migration Steps

  1. Create FTS5 Virtual Tables

    CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
        noteId UNINDEXED,
        title,
        content,
        tokenize = 'trigram',
        detail = 'full'
    );
  2. Populate FTS Tables

    • Batch processing (100 notes at a time)
    • Excludes protected notes (isProtected = 0)
    • Excludes deleted notes (isDeleted = 0)
    • Only indexes searchable types: text, code, mermaid, canvas, mindMap
  3. Create Synchronization Triggers (12 total)

    Trigger Event Action
    notes_fts_insert INSERT on notes Add to FTS
    notes_fts_update UPDATE on notes Update FTS
    notes_fts_delete DELETE on notes Remove from FTS
    notes_fts_soft_delete isDeleted changes Remove from FTS
    notes_fts_protect isProtected = 1 Remove from FTS
    notes_fts_unprotect isProtected = 0 Add to FTS
    notes_fts_blob_insert INSERT on blobs Update FTS content
    notes_fts_blob_update UPDATE on blobs Update FTS content
    attributes_fts_insert INSERT on attributes Add to FTS
    attributes_fts_update UPDATE on attributes Update FTS
    attributes_fts_delete DELETE on attributes Remove from FTS
    attributes_fts_soft_delete isDeleted changes Remove from FTS
  4. Create Performance Indexes

    • 18 strategic indexes on frequently queried columns
    • Covering indexes for common query patterns
  5. Run ANALYZE

    • Updates SQLite query planner statistics
    • Optimizes query execution plans
  6. Cleanup Legacy Tables

    • Drops note_search_content and note_tokens
    • Removes related entity_changes records

Rollback Strategy

If migration fails:

  1. Transaction rolls back all changes
  2. FTS tables are cleaned up automatically
  3. Indexes are dropped via DROP INDEX IF EXISTS
  4. Original tables remain unchanged

To manually rollback after successful migration:

DROP TABLE IF EXISTS notes_fts;
DROP TABLE IF EXISTS attributes_fts;
-- Drop all triggers (12 DROP TRIGGER statements)
-- Drop all new indexes (18 DROP INDEX statements)

New Test Files (16 spec files, ~9,000 lines)

Test File Coverage Area Lines
fts5_integration.spec.ts FTS5 integration tests 806
fts_search.spec.ts Core FTS search operations 1,463
operators.spec.ts Search operator tests 1,114
fuzzy_search.spec.ts Fuzzy matching tests 867
attribute_search.spec.ts Attribute FTS tests 688
hierarchy_search.spec.ts Hierarchy traversal tests 607
logical_operators.spec.ts AND/OR/NOT tests 561
edge_cases.spec.ts Edge case handling 518
search_results.spec.ts Result formatting tests 493
special_features.spec.ts Special search features 488
progressive_search.spec.ts Progressive search tests 419
content_search.spec.ts Content search tests 399
property_search.spec.ts Property search tests 823
sqlite_functions.spec.ts Custom SQL functions 113
etapi/search.spec.ts ETAPI search endpoint 356+

Test Helpers and Fixtures

File Purpose
test/search_test_helpers.ts Test setup utilities
test/search_fixtures.ts Test data fixtures
test/search_assertion_helpers.ts Custom assertions

Stress Testing

New stress test script: scripts/stress-test-populate.ts (512 lines)

  • Populates database with configurable note counts
  • Tests search performance under load
  • Validates FTS index integrity

Running Tests

# All tests (includes new search tests)
pnpm test:all

# Just sequential tests (server tests run sequentially)
pnpm test:sequential

# Generate coverage
pnpm coverage

Architecture Overview

New FTS Module Structure

apps/server/src/services/search/
├── fts/
│   ├── index.ts           # Module exports
│   ├── types.ts           # TypeScript interfaces
│   ├── errors.ts          # Custom error types
│   ├── search_service.ts  # Core search operations
│   ├── query_builder.ts   # FTS5 query construction
│   └── index_manager.ts   # FTS index management
├── sqlite_functions.ts    # Custom SQL functions (edit_distance, regex_match)
├── utils/
│   └── text_utils.ts      # Text normalization utilities
└── expressions/
    └── note_content_fulltext.ts  # Integration with search expressions

Integration Points

  1. Search Expressions (note_content_fulltext.ts)

    • Receives search tokens from query parser
    • Calls FTS module for matching
    • Returns matched note IDs to expression evaluator
  2. Search Service (services/search.ts)

    • Orchestrates search flow
    • Handles progressive search (exact -> fuzzy fallback)
    • Extracts content/attribute snippets
  3. SQL Module (sql.ts)

    • Registers custom SQLite functions
    • Provides database connection to FTS module

Custom SQLite Functions

Function Purpose Usage
edit_distance(a, b) Levenshtein distance for fuzzy matching WHERE edit_distance(title, ?) <= 2
regex_match(pattern, text) Regular expression matching WHERE regex_match(?, content)

Search Operators Supported

Operator Name Implementation Example
= Exact phrase FTS5 MATCH + word boundary filter note.content = "hello world"
!= Not contains NOT MATCH / NOT LIKE note.content != "test"
*=* Contains LIKE with trigram optimization note.content *=* "partial"
*= Starts with LIKE with trigram optimization note.content *= "hello"
=* Ends with LIKE with trigram optimization note.content =* "world"
~= Fuzzy FTS5 OR + JS scoring note.content ~= "aproximate"
~* Fuzzy starts with FTS5 OR + JS scoring note.content ~* "helo"
%= Regex Traditional (not FTS5) note.content %= "^test.*$"

Files Changed Summary

Core FTS Implementation (6 files)

File Description Lines
fts/index.ts Module exports 90
fts/types.ts TypeScript interfaces 62
fts/errors.ts Custom error types 30
fts/search_service.ts Core search operations 655
fts/query_builder.ts Query construction 154
fts/index_manager.ts Index management 262

Integration Files (4 files)

File Description Changes
expressions/note_content_fulltext.ts Search expression integration +270 lines
services/search.ts Search orchestration Modified
services/build_comparator.ts Comparator updates Modified
sqlite_functions.ts Custom SQL functions 284 lines

Migration (1 file)

File Description Lines
migrations/0234__add_fts5_search.ts Database migration 643

Tests (17 files, ~9,000 lines)

See Testing Requirements section above.


feat(search): don't limit the number of blobs to put in virtual tables

fix(search): improve FTS triggers to handle all SQL operations correctly

The root cause of FTS index issues during import was that database triggers
weren't properly handling all SQL operations, particularly upsert operations
(INSERT ... ON CONFLICT ... DO UPDATE) that are commonly used during imports.

Key improvements:
- Fixed INSERT trigger to handle INSERT OR REPLACE operations
- Updated UPDATE trigger to fire on ANY change (not just specific columns)
- Improved blob triggers to use INSERT OR REPLACE for atomic updates
- Added proper handling for notes created before their blobs (import scenario)
- Added triggers for protection state changes
- All triggers now use LEFT JOIN to handle missing blobs gracefully

This ensures the FTS index stays synchronized even when:
- Entity events are disabled during import
- Notes are re-imported (upsert operations)
- Blobs are deduplicated across notes
- Notes are created before their content blobs

The solution works entirely at the database level through triggers,
removing the need for application-level workarounds.

fix(search): consolidate FTS trigger fixes into migration 234

- Merged improved trigger logic from migration 235 into 234
- Deleted unnecessary migration 235 since DB version is still 234
- Ensures triggers handle all SQL operations (INSERT OR REPLACE, upserts)
- Fixes FTS indexing for imported notes by handling missing blobs
- Schema.sql and migration 234 now have identical trigger implementations
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Aug 30, 2025
@perfectra1n perfectra1n marked this pull request as draft September 2, 2025 05:08
Comment on lines 207 to 208
CREATE INDEX IDX_entity_changes_component
ON entity_changes (componentId, utcDateChanged DESC);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you double-check if a component index is really needed? The component ID is used on the client side to distinguish which UI element made the change to avoid accidentally updating the very same editor that the user is using.

On the server side I don't think the components are really necessary.

Comment on lines 18 to 28
// Verify SQLite version supports trigram tokenizer (requires 3.34.0+)
const sqliteVersion = sql.getValue<string>(`SELECT sqlite_version()`);
const [major, minor, patch] = sqliteVersion.split('.').map(Number);
const versionNumber = major * 10000 + minor * 100 + (patch || 0);
const requiredVersion = 3 * 10000 + 34 * 100 + 0; // 3.34.0

if (versionNumber < requiredVersion) {
log.error(`SQLite version ${sqliteVersion} does not support trigram tokenizer (requires 3.34.0+)`);
log.info("Skipping FTS5 trigram migration - will use fallback search implementation");
return; // Skip FTS5 setup, rely on fallback search
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary at runtime? Our server uses pinned versions so as long as the version is correct, there's no need for runtime check.

Comment on lines 38 to 39
-- Drop existing FTS table if it exists (for re-running migration in dev)
DROP TABLE IF EXISTS notes_fts;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't re-run migrations in dev, so that would be unnecessary.

Comment on lines 41 to 50
-- Create FTS5 virtual table with trigram tokenizer
-- Trigram tokenizer provides language-agnostic substring matching:
-- 1. Fast substring matching (50-100x speedup for LIKE queries without wildcards)
-- 2. Case-insensitive search without custom collation
-- 3. No language-specific stemming assumptions (works for all languages)
-- 4. Boolean operators (AND, OR, NOT) and phrase matching with quotes
--
-- IMPORTANT: Trigram requires minimum 3-character tokens for matching
-- detail='none' reduces index size by ~50% while maintaining MATCH/rank performance
-- (loses position info for highlight() function, but snippet() still works)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments could be moved out of the SQL statement and into the code to avoid embedding them at build time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since migration 235 doesn't exist anymore, why not merge it with 0234 into a single migration?

Comment on lines 188 to 192
// Additional validation: ensure token doesn't contain SQL injection attempts
if (sanitized.includes(';') || sanitized.includes('--')) {
log.error(`Potential SQL injection attempt detected in token: "${token}"`);
return "__invalid_token__";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we simply escape the characters instead of dismissing the search entirely? Some people might complain that their search doesn't work properly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is too big, consider splitting it into... something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this performance monitoring mechanism?

import { AppInfo } from "@triliumnext/commons";

const APP_DB_VERSION = 233;
const APP_DB_VERSION = 236;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to revert the version to 234 if you join the migrations together.

Comment on lines 392 to 394
function getDbConnection(): DatabaseType {
return dbConnection;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels unsafe. ☠️

* @returns String with LIKE wildcards escaped
*/
export function escapeLikeWildcards(str: string): string {
return str.replace(/[%_]/g, '\\$&');

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.

Copilot Autofix

AI 3 months ago

To fix the problem, we must ensure that any backslashes in the input string are themselves escaped (turned into \\) before any other escaping occurs, as per established best practices for escaping SQL LIKE wildcards. The correct sequence is:

  1. Escape backslashes (\ becomes \\).
  2. Escape % and _ (% becomes \%, _ becomes \_).

This can be accomplished by first replacing all backslashes with double backslashes, then replacing % and _ with their backslash-prefixed equivalents. This should be done in the escapeLikeWildcards function, which is at lines 88–90 in apps/server/src/services/search/fts/query_builder.ts.

No additional imports are needed, as standard JS replace suffices.


Suggested changeset 1
apps/server/src/services/search/fts/query_builder.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/server/src/services/search/fts/query_builder.ts b/apps/server/src/services/search/fts/query_builder.ts
--- a/apps/server/src/services/search/fts/query_builder.ts
+++ b/apps/server/src/services/search/fts/query_builder.ts
@@ -86,7 +86,8 @@
  * @returns String with LIKE wildcards escaped
  */
 export function escapeLikeWildcards(str: string): string {
-    return str.replace(/[%_]/g, '\\$&');
+    // Escape backslash, then escape % and _
+    return str.replace(/\\/g, '\\\\').replace(/[%_]/g, '\\$&');
 }
 
 /**
EOF
@@ -86,7 +86,8 @@
* @returns String with LIKE wildcards escaped
*/
export function escapeLikeWildcards(str: string): string {
return str.replace(/[%_]/g, '\\$&');
// Escape backslash, then escape % and _
return str.replace(/\\/g, '\\\\').replace(/[%_]/g, '\\$&');
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
@perfectra1n perfectra1n marked this pull request as ready for review December 28, 2025 06:39
@perfectra1n
Copy link
Member Author

Should be ready if you want to do a run through @eliandoran :)

@werererer
Copy link

I’m currently testing and running it; the initial indexing takes quite a while on my collection. It isn’t finished yet, but I’ll report back later with timing results and how it impacts my speed if everything runs smoothly.

Optimizing future migration time is better handled as follow-up work rather than part of this PR. Users with small databases will be minimally affected, while users with large collections can investigate the causes of longer migration times if needed.

Once it gets through review, it might be useful to include in a future release note a warning that migrations can take a significant amount of time for larger collections.

@werererer
Copy link

werererer commented Feb 8, 2026

0234_add_fts5_search.ts must be optimized and cannot be deferred to later PR. It is too slow. On my collection running the migration for a few hours didn't get me to step 2 of the 7 step migration process. Likely cause: the DB queries are not optimized for large collections yet.

Also the code in 0234_add_fts5_search.ts is one large function I would prefer if it was modularized more, by breaking it down into smaller functions. Having everything in one big function makes the code human unfriendly.

@werererer
Copy link

werererer commented Feb 9, 2026

Report for commit 9246a69

Outcome
The Migration now is fine. The code and speed have improved.
First, the Code for 0234_add_fts5_search is modular and human readable.
Second, The Migration took 6 minutes and 50 seconds from an unmigrated database to a migrated one with being able to edit in Trilium. The command used:

TRILIUM_DOCUMENT_PATH=/home/jakob/test/my-trilium-copy2.db pnpm run desktop:start
Output
⋊> ~/g/Trilium-Original on feat/rice-searching-with-sqlite ◦… ~/g/Trilium-Original on feat/rice-searching-with-sqlite ◦ TRILIUM_DOCUMENT_PATH=/home/jakob/test/my-trilium-copy2.db pnpm run desktop:start

> @triliumnext/source@0.101.3 desktop:start /home/jakob/git/Trilium-Original
> pnpm run --filter desktop dev


> @triliumnext/desktop@0.101.3 dev /home/jakob/git/Trilium-Original/apps/desktop
> cross-env TRILIUM_PORT=37742 TRILIUM_DATA_DIR=data tsx ../../scripts/electron-start.mts src/main.ts

Registering SQLite custom functions...
Registered SQLite function: edit_distance
Registered SQLite function: regex_match
SQLite custom functions registration completed (2/2)
SQLite custom search functions initialized successfully
[24899:0209/010314.566002:ERROR:dbus/object_proxy.cc:573] Failed to call method: org.freedesktop.systemd1.Manager.StartTransientUnit: object_path= /org/freedesktop/systemd1: org.freedesktop.systemd1.UnitExists: Unit app-org.chromium.Chromium-24899.scope was already loaded or has a fragment file.

 _____     _ _ _
|_   _| __(_) (_)_   _ _ __ ___   | \ | | ___ | |_ ___  ___
  | || '__| | | | | | | '_ ` _ \  |  \| |/ _ \| __/ _ \/ __|
  | || |  | | | | |_| | | | | | | | |\  | (_) | ||  __/\__ \
  |_||_|  |_|_|_|\__,_|_| |_| |_| |_| \_|\___/ \__\___||___/ 0.101.3

📦 Versions:    app=0.101.3 db=234 sync=36 clipper=1.0
🔧 Build:       2025-06-07 09:45:40 (7cbff47078)
📂 Data dir:    /home/jakob/git/Trilium-Original/apps/desktop/data
⏰ UTC time:    2026-02-09 00:03:13
💻 CPU:         Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz (8-core @ 3299 Mhz)
💾 DB size:     2140.8 MiB

App db version is 234, while db version is 233. Migration needed.
Creating backup...
Checking hidden subtree.
Trusted reverse proxy: false
App HTTP server starting up at port 37742
Listening on port 37742
Server loaded
Starting Electron...
Created backup at /home/jakob/git/Trilium-Original/apps/desktop/data/backup/backup-before-migration.db
Attempting migration to version 234
Migration with JS module
Starting FTS5 and performance optimization migration...
Creating FTS5 virtual table for full-text search...
Populating FTS5 table with existing note content...
Slow query took 12427ms: SELECT COUNT(*) FROM notes n LEFT JOIN blobs b ON n.blobId = b.blobId WHERE n.type IN ('text','code','mermaid','canvas','mindMap') AND n.isDeleted = 0 AND n.isProtected = 0
Indexing 240419 notes into FTS5 (this may take a moment for large databases)...
Slow query took 55256ms: INSERT INTO notes_fts (noteId, title, content) SELECT n.noteId, n.title, COALESCE(b.content, '') FROM notes n LEFT JOIN blobs b ON n.blobId = b.blobId WHERE n.type IN ('text','code','mermaid','canvas','mindMap') AND n.isDeleted = 0 AND n.isProtected = 0
Slow query took 5816ms: INSERT INTO notes_fts(notes_fts) VALUES('optimize')
Completed FTS indexing of 240419 notes in 61077ms
Creating FTS synchronization triggers...
Slow query took 375ms: CREATE TRIGGER notes_fts_insert AFTER INSERT ON notes WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap') AND NEW.isDeleted = 0 AND NEW.isProtected = 0 BEGIN -- First delete any existing FTS entry (in case of INSERT OR REPLACE) DELETE FROM notes_fts WHERE noteId = NEW.noteId; -- Then insert the new entry, using LEFT JOIN to handle missing blobs INSERT INTO notes_fts (noteId, title, content) SELECT NEW.noteId, NEW.title, COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet FROM (SELECT NEW.noteId) AS note_select LEFT JOIN blobs b ON b.blobId = NEW.blobId; END
FTS5 triggers created successfully
Adding strategic performance indexes...
Creating composite index on notes table for search filters...
Creating covering index for note metadata...
Creating index for protected notes...
Creating composite index on branches for tree traversal...
Creating covering index for branch queries...
Creating index for reverse tree lookup...
Creating composite index on attributes for search...
Creating covering index for attribute queries...
Creating index for inherited attributes...
Creating index for label attributes...
Creating index for relation attributes...
Creating index for blob content size...
Creating composite index for attachments...
Creating composite index for revisions...
Creating composite index for entity changes sync...
Creating index for recent notes...
Running ANALYZE to update SQLite query planner statistics...
Performance index creation completed in 53514ms (16 indexes created)
Creating FTS5 index for attributes...
Populating attributes_fts table...
Slow query took 7096ms: INSERT INTO attributes_fts (attributeId, noteId, name, value) SELECT attributeId, noteId, name, COALESCE(value, '') FROM attributes WHERE isDeleted = 0
Slow query took 457ms: INSERT INTO attributes_fts(attributes_fts) VALUES('optimize')
Slow query took 53ms: SELECT COUNT(*) FROM attributes_fts
Populated 328517 attributes in 7559ms
Attributes FTS5 setup completed successfully
Cleaning up legacy custom search tables...
Slow query took 2066ms: DELETE FROM entity_changes WHERE entityName IN ('note_search_content', 'note_tokens')
FTS5 and performance optimization migration completed successfully
Migration to version 234 has been successful.
Becca (note cache) load took 4446ms
Created new note '_lbMobileTabSwitcher', branch '_lbMobileVisibleLaunchers__lbMobileTabSwitcher' of type 'launcher', mime ''
Updating attribute _help_Un4wj2Mak2Ky_ldocName from "User Guide/User Guide/Installation & Setup/Server Installation/Nix flake.clone" to "User Guide/User Guide/Installation & Setup/Desktop Installation/Nix flake"
Slow query took 78ms: INSERT INTO attributes (attributeId, noteId, type, name, position, value, isInheritable, utcDateModified, isDeleted) VALUES (@attributeId, @noteId, @type, @name, @position, @value, @isInheritable, @utcDateModified, @isDeleted) ON CONFLICT (attributeId) DO UPDATE SET attributeId = @attributeId, noteId = @noteId, type = @type, name = @name, position = @position, value = @value, isInheritable = @isInheritable, utcDateModified = @utcDateModified, isDeleted = @isDeleted
Updating attribute _help_Un4wj2Mak2Ky_liconClass from "bx bx-file" to "bx bxl-tux"
Slow query took 72ms: INSERT INTO attributes (attributeId, noteId, type, name, position, value, isInheritable, utcDateModified, isDeleted) VALUES (@attributeId, @noteId, @type, @name, @position, @value, @isInheritable, @utcDateModified, @isDeleted) ON CONFLICT (attributeId) DO UPDATE SET attributeId = @attributeId, noteId = @noteId, type = @type, name = @name, position = @position, value = @value, isInheritable = @isInheritable, utcDateModified = @utcDateModified, isDeleted = @isDeleted
Updating attribute _help_Un4wj2Mak2Ky_ldocName from "User Guide/User Guide/Installation & Setup/Desktop Installation/Nix flake" to "User Guide/User Guide/Installation & Setup/Server Installation/Nix flake.clone"
Slow query took 73ms: INSERT INTO attributes (attributeId, noteId, type, name, position, value, isInheritable, utcDateModified, isDeleted) VALUES (@attributeId, @noteId, @type, @name, @position, @value, @isInheritable, @utcDateModified, @isDeleted) ON CONFLICT (attributeId) DO UPDATE SET attributeId = @attributeId, noteId = @noteId, type = @type, name = @name, position = @position, value = @value, isInheritable = @isInheritable, utcDateModified = @utcDateModified, isDeleted = @isDeleted
Updating attribute _help_Un4wj2Mak2Ky_liconClass from "bx bxl-tux" to "bx bx-file"
Slow query took 74ms: INSERT INTO attributes (attributeId, noteId, type, name, position, value, isInheritable, utcDateModified, isDeleted) VALUES (@attributeId, @noteId, @type, @name, @position, @value, @isInheritable, @utcDateModified, @isDeleted) ON CONFLICT (attributeId) DO UPDATE SET attributeId = @attributeId, noteId = @noteId, type = @type, name = @name, position = @position, value = @value, isInheritable = @isInheritable, utcDateModified = @utcDateModified, isDeleted = @isDeleted
Created new note '_help_wyaGBBQrl4i3', branch '_help_oPVyFC7WL2Lp__help_wyaGBBQrl4i3' of type 'doc', mime ''
Created new note '_help_gOKqSJgXLcIj', branch '_help_Wy267RK4M69c__help_gOKqSJgXLcIj' of type 'doc', mime ''
Created new note '_help_dj3j8dG4th4l', branch '_help_syuSEKf2rUGr__help_dj3j8dG4th4l' of type 'doc', mime ''
Created new note '_help_XJGJrpu7F9sh', branch '_help_W8vYD3Q1zjCR__help_XJGJrpu7F9sh' of type 'doc', mime ''
Created new note '_help_g1mlRoU8CsqC', branch '_help_pKK96zzmvBGf__help_g1mlRoU8CsqC' of type 'doc', mime ''
Created new note '_help_cNpC0ITcfX0N', branch '_help_CdNpE2pqjmI6__help_cNpC0ITcfX0N' of type 'doc', mime ''
Registered global shortcut Ctrl+Alt+P for action createNoteIntoInbox
[baseline-browser-mapping] The data in this module is over two months old.  To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
CSRF token generation: Successful
Slow query took 132ms: SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1
Slow query took 187ms: SELECT COUNT(1) FROM notes
Slow query took 130ms: SELECT COUNT(1) FROM branches
Slow query took 117ms: SELECT COUNT(1) FROM attributes
Table counts: notes: 245974, revisions: 37106, attachments: 6423, branches: 248279, attributes: 328625, etapi_tokens: 5, blobs: 177021
Slow query took 565ms: SELECT noteId as entityId FROM notes LEFT JOIN entity_changes ec ON ec.entityName = 'notes' AND ec.entityId = notes.noteId WHERE ec.id IS NULL
Slow query took 40110ms: SELECT id, entityId FROM entity_changes LEFT JOIN notes ON entityId = notes.noteId WHERE entity_changes.isErased = 0 AND entity_changes.entityName = 'notes' AND notes.noteId IS NULL
Slow query took 707ms: SELECT id, entityId FROM entity_changes JOIN notes ON entityId = notes.noteId WHERE entity_changes.isErased = 1 AND entity_changes.entityName = 'notes'
Slow query took 70ms: SELECT revisionId as entityId FROM revisions LEFT JOIN entity_changes ec ON ec.entityName = 'revisions' AND ec.entityId = revisions.revisionId WHERE ec.id IS NULL
Slow query took 176ms: SELECT id, entityId FROM entity_changes LEFT JOIN revisions ON entityId = revisions.revisionId WHERE entity_changes.isErased = 0 AND entity_changes.entityName = 'revisions' AND revisions.revisionId IS NULL
Slow query took 36ms: SELECT id, entityId FROM entity_changes JOIN revisions ON entityId = revisions.revisionId WHERE entity_changes.isErased = 1 AND entity_changes.entityName = 'revisions'
Slow query took 20ms: SELECT id, entityId FROM entity_changes LEFT JOIN attachments ON entityId = attachments.attachmentId WHERE entity_changes.isErased = 0 AND entity_changes.entityName = 'attachments' AND attachments.attachmentId IS NULL
Slow query took 318ms: SELECT blobId as entityId FROM blobs LEFT JOIN entity_changes ec ON ec.entityName = 'blobs' AND ec.entityId = blobs.blobId WHERE ec.id IS NULL
Slow query took 711ms: SELECT id, entityId FROM entity_changes LEFT JOIN blobs ON entityId = blobs.blobId WHERE entity_changes.isErased = 0 AND entity_changes.entityName = 'blobs' AND blobs.blobId IS NULL
Slow query took 76ms: SELECT id, entityId FROM entity_changes JOIN blobs ON entityId = blobs.blobId WHERE entity_changes.isErased = 1 AND entity_changes.entityName = 'blobs'
Slow query took 893ms: SELECT branchId as entityId FROM branches LEFT JOIN entity_changes ec ON ec.entityName = 'branches' AND ec.entityId = branches.branchId WHERE ec.id IS NULL
Slow query took 1189ms: SELECT id, entityId FROM entity_changes LEFT JOIN branches ON entityId = branches.branchId WHERE entity_changes.isErased = 0 AND entity_changes.entityName = 'branches' AND branches.branchId IS NULL
Slow query took 156ms: SELECT id, entityId FROM entity_changes JOIN branches ON entityId = branches.branchId WHERE entity_changes.isErased = 1 AND entity_changes.entityName = 'branches'
Slow query took 510ms: SELECT attributeId as entityId FROM attributes LEFT JOIN entity_changes ec ON ec.entityName = 'attributes' AND ec.entityId = attributes.attributeId WHERE ec.id IS NULL
Slow query took 3689ms: SELECT id, entityId FROM entity_changes LEFT JOIN attributes ON entityId = attributes.attributeId WHERE entity_changes.isErased = 0 AND entity_changes.entityName = 'attributes' AND attributes.attributeId IS NULL
Slow query took 185ms: SELECT id, entityId FROM entity_changes JOIN attributes ON entityId = attributes.attributeId WHERE entity_changes.isErased = 1 AND entity_changes.entityName = 'attributes'
Slow query took 174ms: SELECT branchId, branches.noteId FROM branches LEFT JOIN notes USING (noteId) WHERE branches.isDeleted = 0 AND notes.noteId IS NULL
Slow query took 144ms: SELECT branchId, branches.parentNoteId AS parentNoteId FROM branches LEFT JOIN notes ON notes.noteId = branches.parentNoteId WHERE branches.isDeleted = 0 AND branches.noteId != 'root' AND notes.noteId IS NULL
Slow query took 381ms: SELECT attributeId, attributes.noteId FROM attributes LEFT JOIN notes USING (noteId) WHERE attributes.isDeleted = 0 AND notes.noteId IS NULL
Slow query took 101ms: SELECT attachmentId, attachments.ownerId AS noteId FROM attachments WHERE attachments.ownerId NOT IN ( SELECT noteId FROM notes UNION ALL SELECT revisionId FROM revisions ) AND attachments.isDeleted = 0
Slow query took 476ms: SELECT DISTINCT notes.noteId FROM notes LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0 WHERE notes.isDeleted = 0 AND branches.branchId IS NULL
Slow query took 67ms: SELECT noteId, parentNoteId FROM branches WHERE branches.isDeleted = 0 GROUP BY branches.parentNoteId, branches.noteId HAVING COUNT(1) > 1
Slow query took 86ms: SELECT noteId, type FROM notes WHERE isDeleted = 0 AND type NOT IN ('text', 'code', 'render', 'file', 'image', 'search', 'relationMap', 'book', 'noteMap', 'mermaid', 'canvas', 'webView', 'launcher', 'doc', 'contentWidget', 'mindMap', 'aiChat')
Slow query took 162ms: SELECT notes.noteId, notes.isProtected, notes.type, notes.mime FROM notes LEFT JOIN blobs USING (blobId) WHERE blobs.blobId IS NULL AND notes.isDeleted = 0
Slow query took 137ms: SELECT revisions.revisionId, blobs.blobId FROM revisions LEFT JOIN blobs USING (blobId) WHERE blobs.blobId IS NULL
Slow query took 44ms: SELECT parentNoteId FROM branches JOIN notes ON notes.noteId = branches.parentNoteId WHERE notes.isDeleted = 0 AND notes.type == 'search' AND branches.isDeleted = 0
Slow query took 60ms: SELECT attributeId, type FROM attributes WHERE isDeleted = 0 AND type != 'label' AND type != 'relation'
Slow query took 576ms: SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0
All consistency checks passed with no errors detected (took 53231ms)
(node:24899) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
(Use `electron --trace-deprecation ...` to show where the warning was created)
websocket client connected
Slow 200 GET /api/tree with 2645465 bytes took 43ms
200 GET /api/options with 14400 bytes took 2ms
200 GET /api/keyboard-actions with 21992 bytes took 5ms
Slow query took 90ms: SELECT COUNT(*) AS count FROM notes WHERE isDeleted = 0;
Slow 200 GET /api/autocomplete/notesCount with 6 bytes took 92ms
200 GET /api/options/locales with 1163 bytes took 0ms
Slow 200 GET /api/script/widgets with 2 bytes took 230ms
Sending message to all clients: {"type":"sync-push-in-progress","lastSyncedPush":8566473}
Sync ADzkvyKcjD: Pushing 29 sync changes in 657ms
Nothing to push
Sending message to all clients: {"type":"sync-pull-in-progress","lastSyncedPush":8566512}
Slow query took 76ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 71ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 1657ms: DELETE FROM notes WHERE noteId = ?
Slow query took 997ms: DELETE FROM notes WHERE noteId = ?
Slow query took 102ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 74ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 73ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 894ms: DELETE FROM notes WHERE noteId = ?
Slow query took 838ms: DELETE FROM notes WHERE noteId = ?
Slow query took 844ms: DELETE FROM notes WHERE noteId = ?
Slow query took 367ms: DELETE FROM notes WHERE noteId = ?
Slow query took 133ms: DELETE FROM notes WHERE noteId = ?
Slow query took 73ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 73ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 71ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 74ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 73ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 71ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 71ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 71ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 72ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 71ms: DELETE FROM attributes WHERE attributeId = ?
Slow query took 355ms: INSERT OR REPLACE INTO notes ( noteId, title, isProtected, type, mime, blobId, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Slow query took 134ms: INSERT OR REPLACE INTO notes ( noteId, title, isProtected, type, mime, blobId, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Slow query took 128ms: INSERT OR REPLACE INTO notes ( noteId, title, isProtected, type, mime, blobId, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Slow query took 136ms: INSERT OR REPLACE INTO notes ( noteId, title, isProtected, type, mime, blobId, isDeleted, deleteId, dateCreated, dateModified, utcDateCreated, utcDateModified ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
updated: {revisions: [SyHjsfIp6aIh, 2qXqc0HVCq1A, 4CpDCjPKMXEa], blobs: [MDzJ0O7PrQqu90rzEpS7, RFYqVRWkEpsNSAHqIGbI, jNCb0b31LR17XMkSQzcn], notes: [jLouL2u6ytOP, EoqBQhFdgVoY, wSzi3jROlERP, xZIR5fCUvDTJ], branches: [CQ2aaKA1i47R_EoqBQhFdgVoY], attributes: [CVVB3lW9NKdv]}, alreadyUpdated: 0, erased: 54, alreadyErased: 0
Sync xi2rz5eLR4: Pulled 66 changes in 24 KB, starting at entityChangeId=6782511 in 628ms and applied them in 8553ms, 0 outstanding pulls
Finished pull
Nothing to push
200 POST /api/tree/load with 319255 bytes took 6ms
200 POST /api/tree/load with 61361 bytes took 2ms
200 GET /api/keyboard-shortcuts-for-notes with 2 bytes took 0ms
200 GET /api/system-checks with 27 bytes took 0ms
200 POST /api/tree/load with 12011 bytes took 1ms
200 POST /api/tree/load with 12038 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
Slow query took 386ms: SELECT notes.noteId, notes.isDeleted AS current_isDeleted, notes.deleteId AS current_deleteId, notes.title AS current_title, notes.isProtected AS current_isProtected, revisions.title, revisions.utcDateCreated AS utcDate, revisions.dateCreated AS date FROM revisions JOIN notes USING(noteId)
Slow query took 587ms: SELECT notes.noteId, notes.isDeleted AS current_isDeleted, notes.deleteId AS current_deleteId, notes.title AS current_title, notes.isProtected AS current_isProtected, notes.title, notes.utcDateCreated AS utcDate, -- different from the second SELECT notes.dateCreated AS date -- different from the second SELECT FROM notes UNION ALL SELECT notes.noteId, notes.isDeleted AS current_isDeleted, notes.deleteId AS current_deleteId, notes.title AS current_title, notes.isProtected AS current_isProtected, notes.title, notes.utcDateModified AS utcDate, -- different from the first SELECT notes.dateModified AS date -- different from the first SELECT FROM notes WHERE notes.isDeleted = 1
Slow 200 GET /api/recent-changes/undefined with 2 bytes took 2168ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 8047 bytes took 1ms
200 POST /api/tree/load with 8047 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 GET /api/notes/uwWo1CW0aJPg/blob with 2600 bytes took 1ms
200 GET /api/notes/uwWo1CW0aJPg/metadata with 181 bytes took 1ms
200 GET /api/notes/uwWo1CW0aJPg/attachments with 2 bytes took 1ms
200 GET /api/notes/ivdnLxVB0tRG/metadata with 181 bytes took 0ms
Slow 200 GET /api/search/note.parents.noteId%3D%22ivdnLxVB0tRG%22%20%23!archived with 31 bytes took 806ms
200 GET /api/notes/ivdnLxVB0tRG/attachments with 2 bytes took 2ms
200 GET /api/notes/hSjx2869G8mp/metadata with 181 bytes took 0ms
200 GET /api/notes/hSjx2869G8mp/attachments with 2 bytes took 0ms
200 GET /api/notes/cp8yS8WVM6b0/metadata with 181 bytes took 0ms
200 GET /api/notes/cp8yS8WVM6b0/attachments with 2 bytes took 0ms
200 GET /api/notes/cp8yS8WVM6b0/metadata with 181 bytes took 1ms
200 GET /api/notes/cp8yS8WVM6b0/attachments with 2 bytes took 0ms
200 GET /api/notes/kjCNM4ZsXtbO/metadata with 181 bytes took 1ms
200 GET /api/notes/kjCNM4ZsXtbO/attachments with 2 bytes took 0ms
200 GET /api/notes/qInlYeWeyY1s/metadata with 181 bytes took 0ms
200 GET /api/notes/qInlYeWeyY1s/attachments with 2 bytes took 1ms
200 GET /api/notes/WfvvWhnD1G8Y/metadata with 181 bytes took 0ms
200 GET /api/notes/WfvvWhnD1G8Y/attachments with 2 bytes took 1ms
200 GET /api/notes/019Irg08gmps/metadata with 181 bytes took 0ms
200 GET /api/notes/019Irg08gmps/attachments with 2 bytes took 1ms
200 GET /api/notes/OtFLl2tc36Fa/metadata with 181 bytes took 0ms
Slow 200 GET /api/search/note.parents.noteId%3D%22OtFLl2tc36Fa%22%20%23!archived with 16 bytes took 621ms
200 GET /api/notes/OtFLl2tc36Fa/attachments with 2 bytes took 0ms
200 GET /api/notes/1MPJV62dL8uQ/metadata with 181 bytes took 1ms
Slow 200 GET /api/search/note.parents.noteId%3D%221MPJV62dL8uQ%22%20%23!archived with 16 bytes took 583ms
200 GET /api/notes/1MPJV62dL8uQ/attachments with 2 bytes took 0ms
200 GET /api/notes/2SqhsRocmDB8/metadata with 181 bytes took 0ms
200 GET /api/notes/2SqhsRocmDB8/attachments with 348 bytes took 3ms
200 GET /api/notes/mOn7WJux32LH/metadata with 181 bytes took 1ms
200 GET /api/notes/mOn7WJux32LH/attachments with 2 bytes took 0ms
200 GET /api/notes/ls5NiqEWFkVC/metadata with 181 bytes took 0ms
Slow 200 GET /api/search/note.parents.noteId%3D%22ls5NiqEWFkVC%22%20%23!archived with 16 bytes took 587ms
200 GET /api/notes/ls5NiqEWFkVC/attachments with 1064 bytes took 3ms
200 GET /api/notes/PcQqhPottaQz/metadata with 181 bytes took 1ms
Slow 200 GET /api/search/note.parents.noteId%3D%22PcQqhPottaQz%22%20%23!archived with 16 bytes took 594ms
200 GET /api/notes/PcQqhPottaQz/attachments with 2 bytes took 1ms
200 GET /api/notes/9217kiNdTKZy/metadata with 181 bytes took 0ms
200 GET /api/notes/9217kiNdTKZy/attachments with 2 bytes took 0ms
200 GET /api/notes/SXDojfO5OEqi/metadata with 181 bytes took 0ms
200 GET /api/notes/SXDojfO5OEqi/attachments with 2 bytes took 1ms
200 GET /api/notes/uuoTIP8Ua7OY/metadata with 181 bytes took 0ms
200 GET /api/notes/uuoTIP8Ua7OY/attachments with 2 bytes took 1ms
200 GET /api/notes/hSjx2869G8mp/metadata with 181 bytes took 0ms
Slow 200 GET /api/search/note.parents.noteId%3D%22hSjx2869G8mp%22%20%23!archived with 16 bytes took 585ms
200 GET /api/notes/hSjx2869G8mp/attachments with 2 bytes took 0ms
200 GET /api/notes/uwWo1CW0aJPg/attachments with 2 bytes took 0ms
200 GET /api/note-map/uwWo1CW0aJPg/backlink-count with 11 bytes took 1ms
200 GET /api/notes/3yG6uAk2Ah9a/metadata with 181 bytes took 0ms
200 GET /api/notes/3yG6uAk2Ah9a/attachments with 2 bytes took 1ms
200 GET /api/notes/sRRJH296Si0H/metadata with 181 bytes took 0ms
Slow 200 GET /api/search/note.parents.noteId%3D%22sRRJH296Si0H%22%20%23!archived with 16 bytes took 581ms
200 GET /api/notes/sRRJH296Si0H/attachments with 2 bytes took 0ms
200 GET /api/notes/cHw2qTQkBkQ3/metadata with 181 bytes took 0ms
200 GET /api/notes/cHw2qTQkBkQ3/attachments with 2 bytes took 0ms
Slow 200 GET /api/script/startup with 2 bytes took 204ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 POST /api/tree/load with 18736 bytes took 0ms
200 POST /api/tree/load with 18736 bytes took 1ms
200 GET /api/notes/root with 315 bytes took 1ms
Slow 200 POST /api/note-map/hSjx2869G8mp/link with 5082 bytes took 252ms
200 GET /api/notes/_template_text_snippet with 341 bytes took 0ms
200 GET /api/notes/ls5NiqEWFkVC/blob with 1126 bytes took 1ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 226ms
200 GET /api/notes/2SqhsRocmDB8/blob with 1370 bytes took 1ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 218ms
200 GET /api/notes/PcQqhPottaQz/blob with 928 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 232ms
200 GET /api/notes/qInlYeWeyY1s/blob with 1063 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 228ms
200 GET /api/notes/1MPJV62dL8uQ/blob with 155 bytes took 2ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 218ms
200 GET /api/notes/mOn7WJux32LH/blob with 173 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 211ms
200 GET /api/notes/9217kiNdTKZy/blob with 155 bytes took 1ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 228ms
200 GET /api/notes/sRRJH296Si0H/blob with 1557 bytes took 1ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 223ms
200 GET /api/notes/ivdnLxVB0tRG/blob with 155 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 208ms
200 GET /api/notes/SXDojfO5OEqi/blob with 155 bytes took 1ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 224ms
200 GET /api/notes/kjCNM4ZsXtbO/blob with 3387 bytes took 2ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 223ms
200 GET /api/notes/OtFLl2tc36Fa/blob with 155 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 219ms
200 GET /api/notes/3yG6uAk2Ah9a/blob with 155 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 219ms
200 GET /api/notes/hSjx2869G8mp/blob with 1681 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 228ms
200 GET /api/notes/cp8yS8WVM6b0/blob with 1244 bytes took 2ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 219ms
200 GET /api/notes/019Irg08gmps/blob with 574 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 222ms
200 GET /api/notes/cHw2qTQkBkQ3/blob with 226 bytes took 2ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 223ms
200 GET /api/notes/uuoTIP8Ua7OY/blob with 155 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 229ms
200 GET /api/notes/WfvvWhnD1G8Y/blob with 155 bytes took 0ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 232ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 225ms
Slow 200 GET /api/search/%23textSnippet with 436 bytes took 211ms
Slow 200 GET /api/search/%23workspace%20%23!template with 151 bytes took 221ms
Slow 200 GET /api/autocomplete?query=&activeNoteId=uwWo1CW0aJPg&fastSearch=true with 28869 bytes took 14ms
Slow 200 POST /api/tree/load with 695896 bytes took 12ms
Slow 200 POST /api/tree/load with 695896 bytes took 11ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 11ms
Slow 200 POST /api/tree/load with 695896 bytes took 11ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 11ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 13ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 12ms
Slow 200 POST /api/tree/load with 695896 bytes took 13ms
Slow 200 POST /api/tree/load with 695896 bytes took 12ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
200 POST /api/tree/load with 695896 bytes took 9ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
200 POST /api/tree/load with 695896 bytes took 9ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
Slow 200 POST /api/tree/load with 695896 bytes took 10ms
200 POST /api/tree/load with 283670 bytes took 6ms
200 GET /api/notes/_template_list_view with 326 bytes took 0ms
Slow 200 GET /api/search-templates with 91 bytes took 216ms
Slow 200 GET /api/search-templates with 91 bytes took 220ms
Slow 200 GET /api/search-templates with 91 bytes took 222ms
Slow 200 GET /api/search-templates with 91 bytes took 223ms
Slow 200 GET /api/search-templates with 91 bytes took 216ms
Slow 200 GET /api/search-templates with 91 bytes took 222ms
Slow 200 GET /api/search-templates with 91 bytes took 216ms
Slow 200 GET /api/search-templates with 91 bytes took 225ms
Slow 200 POST /api/tree/load with 44753 bytes took 12ms
200 POST /api/tree/load with 44753 bytes took 7ms
200 POST /api/tree/load with 44753 bytes took 6ms
200 POST /api/tree/load with 44753 bytes took 2ms
200 POST /api/tree/load with 44753 bytes took 3ms
200 POST /api/tree/load with 44753 bytes took 2ms
200 POST /api/tree/load with 44753 bytes took 1ms
200 POST /api/tree/load with 44753 bytes took 1ms
200 GET /api/notes/uY7CjJ2OekAn/blob with 529 bytes took 2ms
200 GET /api/notes/_template_grid_view with 326 bytes took 0ms
200 GET /api/notes/7X0xYEVpValM/blob with 437 bytes took 1ms
200 GET /api/notes/_template_calendar with 324 bytes took 0ms
200 GET /api/notes/8zaUSNpWJDHz/blob with 393 bytes took 1ms
200 GET /api/notes/_template_table with 318 bytes took 0ms
200 GET /api/notes/MvUFGIaCx4Xi/blob with 463 bytes took 0ms
200 GET /api/notes/_template_geo_map with 322 bytes took 1ms
200 GET /api/notes/rmXOTcSJy0lm/blob with 386 bytes took 0ms
200 GET /api/notes/_template_board with 325 bytes took 0ms
200 GET /api/notes/n9TXArgMCwdY/blob with 444 bytes took 0ms
200 GET /api/notes/_template_presentation with 332 bytes took 0ms
200 GET /api/notes/942Bapqd4oJx/blob with 388 bytes took 0ms
Slow 200 GET /api/search-templates with 91 bytes took 359ms
200 GET /api/notes/awPDWddnJc78/blob with 391 bytes took 7ms
200 GET /api/notes/MDUSKyNJBs8n with 326 bytes took 0ms
200 GET /api/notes/AbweExyfkhUS/blob with 392 bytes took 0ms
200 GET /api/notes/CP8hgX8aCiD5 with 341 bytes took 1ms
200 GET /api/notes/couS6SOWVxen/blob with 422 bytes took 1ms
200 GET /api/notes/XOlsDCTcwwA6 with 341 bytes took 0ms
200 GET /api/notes/vyzikqXxj39a/blob with 388 bytes took 0ms
200 GET /api/notes/870tRallGZoK with 329 bytes took 0ms
200 GET /api/notes/BC5G1EkD1Qbn/blob with 361 bytes took 1ms
200 GET /api/notes/esO1STYrAuRC with 357 bytes took 0ms
200 GET /api/notes/xC3IM3CGmfyu/blob with 392 bytes took 1ms
200 GET /api/notes/DhW6204hITfj with 382 bytes took 5ms
200 GET /api/notes/pPCkNJvLA4te/blob with 388 bytes took 1ms
200 GET /api/notes/ugq2ba8SYKad/blob with 389 bytes took 0ms
200 GET /api/notes/h5C9nUB5bEeT/blob with 434 bytes took 0ms
200 GET /api/notes/wencva3WYw6U/blob with 578 bytes took 2ms
200 GET /api/notes/uzJQ8hAFHOZ7/blob with 213 bytes took 0ms
200 GET /api/notes/A3TovtwHYdiG/blob with 389 bytes took 0ms
200 GET /api/notes/AcybBAy63FtR/blob with 396 bytes took 1ms
200 GET /api/notes/4kBS0EwyOnCp/blob with 1004 bytes took 0ms
200 GET /api/notes/Te3QfnCSF7zp/blob with 414 bytes took 0ms
200 GET /api/notes/q1RKLZWh4wud/blob with 428 bytes took 1ms
200 GET /api/notes/4SXLVlaUZzgZ/blob with 462 bytes took 1ms
200 GET /api/notes/POvMlxNa89Ba/blob with 404 bytes took 0ms
200 GET /api/notes/frAbeWUlEIwg/blob with 362 bytes took 0ms
200 GET /api/notes/s8KecG5miE95/blob with 366 bytes took 1ms
200 GET /api/notes/iUWgfBtNqP1X/blob with 199 bytes took 1ms
200 GET /api/notes/IB2yrzdRNygE/blob with 161 bytes took 0ms
JS Info: CKEditor state changed to readyh

Measured Factors

  • search speed: no noticable perceived improvement in any search 1. Tested with JumpTo and search in Relations editor.
  • Random Hanging: The time Trilium hangs was improved significantly. In general I noticed 2 processes resulting in hanging: syncing to database and note saving. This reduces the hanging time for at least hanging due to syncing. For note saving I don't have enough experience with it to make a confident statement about.

Bugs with Justifications

  • the JumpTo window didn't center anymore. This code likely didn't introduce it. The same issue can be observed in main at commit 9142f2d.

Footnotes

  1. Search speed seems to rely on Javascript code anyways. Database queries seem to only make up a small proportation of actual search. Something like console.time 2 should be used instead to pinpoint the one causing the most slowdown 3.

  2. Documentation console.time4 and console.timeEnd5. Example 6.

  3. Optimizing without profiling is unlikely to resolve search-speed issues for large collections. "[...] premature optimization is the root of all evil." (Knuth, 1974, p. 268)7.

  4. https://developer.mozilla.org/en-US/docs/Web/API/console/time_static#:~:text=Syntax

  5. https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd_static#:~:text=Syntax

  6. https://www.geeksforgeeks.org/javascript/difference-between-consoletime-and-consoletimeend-in-javascript/#:~:text=Example

  7. Knuth, D. E. (December 1974). Structured Programming with go to Statements. Computing Surveys, Vol. 6, No. 4. https://pic.plover.com/knuth-GOTO.pdf

@perfectra1n
Copy link
Member Author

Thanks for the great report @werererer - can you include more details on what you used to run the server?

@werererer
Copy link

Trilium Server Version: v0.101.3

OS / Kernel:
Ubtuntu 24.04 (noble)
Kernel: 6.8.0-90-generic
vm.swappiness = 10

Hardware:
Intel i5-7500T@ 2.70 GHz (host CPU)
vCPUs exposed: 3 cores
No SMT / 1 thread per core
Hypervisor: KVM (via Proxmox)

RAM
Total: 17GiB
Available: 3.6 GiB
Swap: 91 GiB, 32 GiB used

This VM has 70 services running, that might influence metrics

@werererer
Copy link

werererer commented Feb 9, 2026

Report Appendix: Search in Depth

9246a69 (this PR) vs 9142f2d (main)

I tried to get data on search, so that we get more accurate data, on how the performance changes without using unreliable reference like "perceived performance"

JumpTo:

  • Search Keyword: "Test"
Output 1
this PR
Sending message to all clients: {"type":"sync-finished","lastSyncedPush":8566644}
Slow autocomplete took 871ms
Slow 200 GET /api/autocomplete?query=Test&activeNoteId=uwWo1CW0aJPg&fastSearch=true with 123616 bytes took 876ms
Slow autocomplete took 759ms
Slow 200 GET /api/autocomplete?query=Nothing&activeNoteId=uwWo1CW0aJPg&fastSearch=true with 29404 bytes took 759ms
Slow autocomplete took 778ms
Slow 200 GET /api/autocomplete?query=cool&activeNoteId=uwWo1CW0aJPg&fastSearch=true with 64441 bytes took 780ms
Nothing to push
Finished pull
Nothing to push
200 GET /api/autocomplete?query=&activeNoteId=uwWo1CW0aJPg&fastSearch=true with 6535 bytes took 3ms
Slow query took 310ms: SELECT blobs.blobId FROM blobs LEFT JOIN notes ON notes.blobId = blobs.blobId LEFT JOIN attachments ON attachments.blobId = blobs.blobId LEFT JOIN revisions ON revisions.blobId = blobs.blobId WHERE notes.noteId IS NULL AND attachments.attachmentId IS NULL AND revisions.revisionId IS NULL
Content hash computation took 3342ms
Content hash checks PASSED
Sending message to all clients: {"type":"sync-finished","lastSyncedPush":8566644}
Slow autocomplete took 751ms
Slow 200 GET /api/autocomplete?query=leitfaden&activeNoteId=uwWo1CW0aJPg&fastSearch=true with 3650 bytes took 752ms
main
 NULL AND attachments.attachmentId IS NULL AND revisions.revisionId IS NULL
Content hash computation took 4301ms
Content hash checks PASSED
Sending message to all clients: {"type":"sync-finished","lastSyncedPush":8566605}
Nothing to push
Finished pull
Nothing to push
Slow autocomplete took 892ms
Slow 200 GET /api/autocomplete?query=Test&activeNoteId=piOkBbjHnuD6&fastSearch=true with 123616 bytes took 894ms
Slow query took 363ms: SELECT blobs.blobId FROM blobs LEFT JOIN notes ON notes.blobId = blobs.blobId LEFT JOIN attachments ON attachments.blobId = blobs.blobId LEFT JOIN revisions ON revisions.blobId = blobs.blobId WHERE notes.noteId IS NULL AND attachments.attachmentId IS NULL AND revisions.revisionId IS NULL
Content hash computation took 3496ms
Content hash checks PASSED
Sending message to all clients: {"type":"sync-finished","lastSyncedPush":8566605}
200 GET /api/autocomplete?query=&activeNoteId=piOkBbjHnuD6&fastSearch=true with 7046 bytes took 7ms
200 GET /api/autocomplete?query=&activeNoteId=piOkBbjHnuD6&fastSearch=true with 7046 bytes took 1ms
Slow autocomplete took 856ms
Slow 200 GET /api/autocomplete?query=Test&activeNoteId=piOkBbjHnuD6&fastSearch=true with 123616 bytes took 858ms

Typing into Reference:
Search Keyword: "Test"

Output 2
This PR
200 GET /api/attribute-names/?type=relation&query=T with 1102 bytes took 2ms
200 GET /api/attribute-names/?type=relation&query=Te with 504 bytes took 1ms
200 GET /api/attribute-names/?type=relation&query=Tes with 9 bytes took 6ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 7ms
Slow 200 POST /api/search-related with 24 bytes took 3232ms
Slow 200 POST /api/search-related with 24 bytes took 2691ms
Slow 200 POST /api/search-related with 24 bytes took 2685ms
Slow 200 POST /api/search-related with 24 bytes took 2767ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 3ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 3ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 3ms
Slow 200 POST /api/search-related with 24 bytes took 2701ms
Slow 200 POST /api/search-related with 24 bytes took 2722ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 6ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 4ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 4ms
Nothing to push
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 4ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 3ms
Finished pull
Nothing to push
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 4ms
200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 5ms
main
Slow query took 508ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=myRelation with 2 bytes took 509ms
Slow query took 40ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=myRelation with 2 bytes took 41ms
Slow 200 POST /api/search-related with 3911 bytes took 8002ms
Slow query took 2716ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query= with 1474 bytes took 2718ms
Slow 200 POST /api/tree/load with 83575 bytes took 12ms
Slow 200 POST /api/search-related with 24 bytes took 3285ms
Slow 200 POST /api/search-related with 24 bytes took 3026ms
Slow 200 POST /api/search-related with 24 bytes took 2497ms
Slow 200 POST /api/search-related with 24 bytes took 2612ms
Slow 200 POST /api/search-related with 24 bytes took 2426ms
Slow 200 POST /api/search-related with 24 bytes took 2428ms
Slow 200 POST /api/search-related with 24 bytes took 2540ms
Slow 200 POST /api/search-related with 24 bytes took 2610ms
Slow 200 POST /api/search-related with 24 bytes took 3228ms
Slow 200 POST /api/search-related with 24 bytes took 2725ms
Slow query took 1073ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query= with 1474 bytes took 1075ms
Slow query took 604ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query= with 1474 bytes took 604ms
Slow query took 586ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query= with 1474 bytes took 587ms
Slow query took 566ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query= with 1474 bytes took 566ms
Slow query took 43ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=T with 1102 bytes took 43ms
Slow query took 40ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Te with 504 bytes took 42ms
Slow query took 51ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Tes with 9 bytes took 52ms
Slow query took 44ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 44ms
Slow query took 43ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 43ms
Slow query took 35ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 35ms
Slow 200 POST /api/search-related with 24 bytes took 2618ms
Slow 200 POST /api/search-related with 24 bytes took 2493ms
Slow 200 POST /api/search-related with 24 bytes took 2491ms
Slow 200 POST /api/search-related with 24 bytes took 2423ms
Slow query took 37ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 37ms
Slow query took 32ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 32ms
Slow query took 32ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 32ms
Slow query took 34ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 34ms
Slow query took 31ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 32ms
Slow query took 32ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 32ms
Slow query took 31ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 32ms
Slow query took 31ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 31ms
Slow query took 46ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 46ms
Slow query took 32ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 33ms
Nothing to push
Finished pull
Nothing to push
Slow 200 POST /api/search-related with 24 bytes took 2730ms
Slow query took 72ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 72ms
Slow query took 48ms: SELECT DISTINCT name FROM attributes WHERE isDeleted = 0 AND type = ? AND name LIKE ?
Slow 200 GET /api/attribute-names/?type=relation&query=Test with 2 bytes took 49ms
Slow 200 POST /api/search-related with 24 bytes took 2725ms

Why did Search not improve?

Also when I tested my rust module i analyzed the code with console.time. I identified that more than 90% of search time is actually invested here: "~/git/Trilium/apps/server/src/services/search/services/search.ts" this line:
at around line 286 inside performeSearch:

    const noteSet = expression.execute(allNoteSet, executionContext, searchContext);

More specifically most time is spend inside the execute functions of:
apps/server/src/services/search/expressions/and.ts
apps/server/src/services/search/expressions/note_content_fulltext.ts

I measured with console.time, and smth like this was the result. (Note: speed is incredibly slow because of the many labels, however if you only put a console.time around expression, then you will get similar results):
computeScore.execute: 132.667ms
performSearch.execute: 43.846s
AndExp.execute: 43.713s
expression.execute: 43.713s

Conclusion

I don't see this PR improving where 90% of the performance is lost. So search speed is only improved insignificantly, however other factors around search have been speed up (please compare output 2). Therefore, moving to FTS5 is the correct step. Furthermore I believe the search can in the future be reworked with more direct database calls reaping the benefits.

@perfectra1n
Copy link
Member Author

Yeah, I've been noticing the same things. FTS5 is worth investing in, just not immediately for the traditional "search" capabilities. I agree that the majority of performance is lost around that line in search.ts... I'll be looking more into that soon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflicts size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants