mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 07:46:30 +01:00
Compare commits
1 Commits
feat/push-
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c79760a4a |
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
145
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html
generated
vendored
Normal file
145
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
<h2>Advanced Search Expressions</h2>
|
||||
<p>This guide covers complex search expressions that combine multiple criteria,
|
||||
use advanced operators, and leverage Trilium's relationship system for
|
||||
sophisticated queries.</p>
|
||||
<h2>Complex Query Construction</h2>
|
||||
<h3>Boolean Logic with Parentheses</h3>
|
||||
<p>Use parentheses to group expressions and control evaluation order:</p><pre><code class="language-text-x-trilium-auto">(#book OR #article) AND #author=Tolkien</code></pre>
|
||||
<p>Finds notes that are either books or articles, written by Tolkien.</p><pre><code class="language-text-x-trilium-auto">#project AND (#status=active OR #status=pending)</code></pre>
|
||||
<p>Finds active or pending projects.</p><pre><code class="language-text-x-trilium-auto">meeting AND (#priority=high OR #urgent) AND note.dateCreated >= TODAY-7</code></pre>
|
||||
<p>Finds recent high-priority or urgent meetings.</p>
|
||||
<h3>Negation Patterns</h3>
|
||||
<p>Use <code>NOT</code> or the <code>not()</code> function to exclude certain
|
||||
criteria:</p><pre><code class="language-text-x-trilium-auto">#book AND not(#genre=fiction)</code></pre>
|
||||
<p>Finds non-fiction books.</p><pre><code class="language-text-x-trilium-auto">project AND not(note.isArchived=true)</code></pre>
|
||||
<p>Finds non-archived notes containing "project".</p><pre><code class="language-text-x-trilium-auto">#!completed</code></pre>
|
||||
<p>Short syntax for notes without the "completed" label.</p>
|
||||
<h3>Mixed Search Types</h3>
|
||||
<p>Combine full-text, attribute, and property searches:</p><pre><code class="language-text-x-trilium-auto">development #category=work note.type=text note.dateModified >= TODAY-30</code></pre>
|
||||
<p>Finds text notes about development, categorized as work, modified in the
|
||||
last 30 days.</p>
|
||||
<h2>Advanced Attribute Searches</h2>
|
||||
<h3>Fuzzy Attribute Matching</h3>
|
||||
<p>When fuzzy attribute search is enabled, you can use partial matches:</p><pre><code class="language-text-x-trilium-auto">#lang</code></pre>
|
||||
<p>Matches labels like "language", "languages", "programming-lang", etc.</p><pre><code class="language-text-x-trilium-auto">#category=prog</code></pre>
|
||||
<p>Matches categories like "programming", "progress", "program", etc.</p>
|
||||
<h3>Multiple Attribute Conditions</h3><pre><code class="language-text-x-trilium-auto">#book #author=Tolkien #publicationYear>=1950 #publicationYear<1960</code></pre>
|
||||
<p>Finds Tolkien's books published in the 1950s.</p><pre><code class="language-text-x-trilium-auto">#task #priority=high #status!=completed</code></pre>
|
||||
<p>Finds high-priority incomplete tasks.</p>
|
||||
<h3>Complex Label Value Patterns</h3>
|
||||
<p>Use various operators for sophisticated label matching:</p><pre><code class="language-text-x-trilium-auto">#isbn %= '978-[0-9-]+' </code></pre>
|
||||
<p>Finds notes with ISBN labels matching the pattern (regex).</p><pre><code class="language-text-x-trilium-auto">#email *=* @company.com</code></pre>
|
||||
<p>Finds notes with email labels containing "@company.com".</p><pre><code class="language-text-x-trilium-auto">#version >= 2.0</code></pre>
|
||||
<p>Finds notes with version labels of 2.0 or higher (numeric comparison).</p>
|
||||
<h2>Relationship Traversal</h2>
|
||||
<h3>Basic Relation Queries</h3><pre><code class="language-text-x-trilium-auto">~author.title *=* Tolkien</code></pre>
|
||||
<p>Finds notes with an "author" relation to notes containing "Tolkien" in
|
||||
the title.</p><pre><code class="language-text-x-trilium-auto">~project.labels.status = active</code></pre>
|
||||
<p>Finds notes related to projects with active status.</p>
|
||||
<h3>Multi-Level Relationships</h3><pre><code class="language-text-x-trilium-auto">~author.relations.publisher.title = "Penguin Books"</code></pre>
|
||||
<p>Finds notes authored by someone published by Penguin Books.</p><pre><code class="language-text-x-trilium-auto">~project.children.title *=* documentation</code></pre>
|
||||
<p>Finds notes related to projects that have child notes about documentation.</p>
|
||||
<h3>Relationship Direction</h3><pre><code class="language-text-x-trilium-auto">note.children.title = "Chapter 1"</code></pre>
|
||||
<p>Finds parent notes that have a child titled "Chapter 1".</p><pre><code class="language-text-x-trilium-auto">note.parents.labels.category = book</code></pre>
|
||||
<p>Finds notes whose parents are categorized as books.</p><pre><code class="language-text-x-trilium-auto">note.ancestors.title = "Literature"</code></pre>
|
||||
<p>Finds notes with "Literature" anywhere in their ancestor chain.</p>
|
||||
<h2>Property-Based Searches</h2>
|
||||
<h3>Note Metadata Queries</h3><pre><code class="language-text-x-trilium-auto">note.type=code note.mime=text/javascript note.dateCreated >= MONTH</code></pre>
|
||||
<p>Finds JavaScript code notes created this month.</p><pre><code class="language-text-x-trilium-auto">note.isProtected=true note.contentSize > 1000</code></pre>
|
||||
<p>Finds large protected notes.</p><pre><code class="language-text-x-trilium-auto">note.childrenCount >= 10 note.type=text</code></pre>
|
||||
<p>Finds text notes with many children.</p>
|
||||
<h3>Advanced Property Combinations</h3><pre><code class="language-text-x-trilium-auto">note.parentCount > 1 #template</code></pre>
|
||||
<p>Finds template notes that are cloned in multiple places.</p><pre><code class="language-text-x-trilium-auto">note.attributeCount > 5 note.type=text note.contentSize < 500</code></pre>
|
||||
<p>Finds small text notes with many attributes (heavily tagged short notes).</p><pre><code class="language-text-x-trilium-auto">note.revisionCount > 10 note.dateModified >= TODAY-7</code></pre>
|
||||
<p>Finds frequently edited notes modified recently.</p>
|
||||
<h2>Date and Time Expressions</h2>
|
||||
<h3>Relative Date Calculations</h3><pre><code class="language-text-x-trilium-auto">#dueDate <= TODAY+7 #dueDate >= TODAY</code></pre>
|
||||
<p>Finds tasks due in the next week.</p><pre><code class="language-text-x-trilium-auto">note.dateCreated >= MONTH-2 note.dateCreated < MONTH</code></pre>
|
||||
<p>Finds notes created in the past two months.</p><pre><code class="language-text-x-trilium-auto">#eventDate = YEAR note.dateCreated >= YEAR-1</code></pre>
|
||||
<p>Finds events scheduled for this year that were planned last year.</p>
|
||||
<h3>Complex Date Logic</h3><pre><code class="language-text-x-trilium-auto">(#startDate <= TODAY AND #endDate >= TODAY) OR #status=ongoing</code></pre>
|
||||
<p>Finds current events or ongoing items.</p><pre><code class="language-text-x-trilium-auto">#reminderDate <= NOW+3600 #reminderDate > NOW</code></pre>
|
||||
<p>Finds reminders due in the next hour (using seconds offset).</p>
|
||||
<h2>Fuzzy Search Techniques</h2>
|
||||
<h3>Fuzzy Exact Matching</h3><pre><code class="language-text-x-trilium-auto">#title ~= managment</code></pre>
|
||||
<p>Finds notes with titles like "management" even with typos.</p><pre><code class="language-text-x-trilium-auto">~category.title ~= progaming</code></pre>
|
||||
<p>Finds notes related to categories like "programming" with misspellings.</p>
|
||||
<h3>Fuzzy Contains Matching</h3><pre><code class="language-text-x-trilium-auto">note.content ~* algoritm</code></pre>
|
||||
<p>Finds notes containing words like "algorithm" with spelling variations.</p><pre><code class="language-text-x-trilium-auto">#description ~* recieve</code></pre>
|
||||
<p>Finds notes with descriptions containing "receive" despite the common
|
||||
misspelling.</p>
|
||||
<h3>Progressive Fuzzy Strategy</h3>
|
||||
<p>By default, Trilium uses exact matching first, then fuzzy as fallback:</p><pre><code class="language-text-x-trilium-auto">development project</code></pre>
|
||||
<p>First finds exact matches for "development" and "project", then adds fuzzy
|
||||
matches if needed.</p>
|
||||
<p>To force fuzzy behavior:</p><pre><code class="language-text-x-trilium-auto">#title ~= development #category ~= projet</code></pre>
|
||||
<h2>Ordering and Limiting</h2>
|
||||
<h3>Multiple Sort Criteria</h3><pre><code class="language-text-x-trilium-auto">#book orderBy #publicationYear desc, note.title asc limit 20</code></pre>
|
||||
<p>Orders books by publication year (newest first), then by title alphabetically,
|
||||
limited to 20 results.</p><pre><code class="language-text-x-trilium-auto">#task orderBy #priority desc, #dueDate asc</code></pre>
|
||||
<p>Orders tasks by priority (high first), then by due date (earliest first).</p>
|
||||
<h3>Dynamic Ordering</h3><pre><code class="language-text-x-trilium-auto">#meeting note.dateCreated >= TODAY-30 orderBy note.dateModified desc</code></pre>
|
||||
<p>Finds recent meetings ordered by last modification.</p><pre><code class="language-text-x-trilium-auto">#project #status=active orderBy note.childrenCount desc limit 10</code></pre>
|
||||
<p>Finds the 10 most complex active projects (by number of sub-notes).</p>
|
||||
<h2>Performance Optimization Patterns</h2>
|
||||
<h3>Efficient Query Structure</h3>
|
||||
<p>Start with the most selective criteria:</p><pre><code class="language-text-x-trilium-auto">#book #author=Tolkien note.dateCreated >= 1950-01-01</code></pre>
|
||||
<p>Better than:</p><pre><code class="language-text-x-trilium-auto">note.dateCreated >= 1950-01-01 #book #author=Tolkien</code></pre>
|
||||
<h3>Fast Search for Large Datasets</h3><pre><code class="language-text-x-trilium-auto">#category=project #status=active</code></pre>
|
||||
<p>With fast search enabled, this searches only attributes, not content.</p>
|
||||
<h3>Limiting Expensive Operations</h3><pre><code class="language-text-x-trilium-auto">note.content *=* "complex search term" limit 50</code></pre>
|
||||
<p>Limits content search to prevent performance issues.</p>
|
||||
<h2>Error Handling and Debugging</h2>
|
||||
<h3>Syntax Validation</h3>
|
||||
<p>Invalid syntax produces helpful error messages:</p><pre><code class="language-text-x-trilium-auto">#book AND OR #author=Tolkien</code></pre>
|
||||
<p>Error: "Mixed usage of AND/OR - always use parentheses to group AND/OR
|
||||
expressions."</p>
|
||||
<h3>Debug Mode</h3>
|
||||
<p>Enable debug mode to see how queries are parsed:</p><pre><code class="language-text-x-trilium-auto">#book #author=Tolkien</code></pre>
|
||||
<p>With debug enabled, shows the internal expression tree structure.</p>
|
||||
<h3>Common Pitfalls</h3>
|
||||
<ul>
|
||||
<li>Unescaped special characters: Use quotes or backslashes</li>
|
||||
<li>Missing parentheses in complex boolean expressions</li>
|
||||
<li>Incorrect property names: Use <code>note.title</code> not <code>title</code>
|
||||
</li>
|
||||
<li>Case sensitivity assumptions: All searches are case-insensitive</li>
|
||||
</ul>
|
||||
<h2>Expression Shortcuts</h2>
|
||||
<h3>Label Shortcuts</h3>
|
||||
<p>Full syntax:</p><pre><code class="language-text-x-trilium-auto">note.labels.category = book</code></pre>
|
||||
<p>Shortcut:</p><pre><code class="language-text-x-trilium-auto">#category = book</code></pre>
|
||||
<h3>Relation Shortcuts</h3>
|
||||
<p>Full syntax:</p><pre><code class="language-text-x-trilium-auto">note.relations.author.title *=* Tolkien</code></pre>
|
||||
<p>Shortcut:</p><pre><code class="language-text-x-trilium-auto">~author.title *=* Tolkien</code></pre>
|
||||
<h3>Property Shortcuts</h3>
|
||||
<p>Some properties have convenient shortcuts:</p><pre><code class="language-text-x-trilium-auto">note.text *=* content</code></pre>
|
||||
<p>Searches both title and content for "content".</p>
|
||||
<h2>Real-World Complex Examples</h2>
|
||||
<h3>Project Management</h3><pre><code class="language-text-x-trilium-auto">(#project OR #task) AND #status!=completed AND
|
||||
(#priority=high OR #dueDate <= TODAY+7) AND
|
||||
not(note.isArchived=true)
|
||||
orderBy #priority desc, #dueDate asc</code></pre>
|
||||
<h3>Research Organization</h3><pre><code class="language-text-x-trilium-auto">(#paper OR #article OR #book) AND
|
||||
~author.title *=* smith AND
|
||||
#topic *=* "machine learning" AND
|
||||
note.dateCreated >= YEAR-2
|
||||
orderBy #citationCount desc limit 25</code></pre>
|
||||
<h3>Content Management</h3><pre><code class="language-text-x-trilium-auto">note.type=text AND note.contentSize > 5000 AND
|
||||
#category=documentation AND note.childrenCount >= 3 AND
|
||||
note.dateModified >= MONTH-1
|
||||
orderBy note.dateModified desc</code></pre>
|
||||
<h3>Knowledge Base Maintenance</h3><pre><code class="language-text-x-trilium-auto">note.attributeCount = 0 AND note.childrenCount = 0 AND
|
||||
note.parentCount = 1 AND note.contentSize < 100 AND
|
||||
note.dateModified < TODAY-90</code></pre>
|
||||
<p>Finds potential cleanup candidates: small, untagged, isolated notes not
|
||||
modified in 90 days.</p>
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a href="#root/_help_yAFfA1SAYlr7">Search Examples and Use Cases</a> -
|
||||
Practical applications</li>
|
||||
<li><a href="#root/_help_UUBStSxWzjgA">Saved Searches</a> - Creating reusable
|
||||
search configurations</li>
|
||||
<li><a href="#root/_help_ttFz520bcNLf">Technical Search Details</a> - Implementation
|
||||
details and performance tuning</li>
|
||||
</ul>
|
||||
158
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/README.html
generated
vendored
Normal file
158
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/README.html
generated
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
<h2>Trilium Search Documentation</h2>
|
||||
<p>Welcome to the comprehensive guide for Trilium's powerful search capabilities.
|
||||
This documentation covers everything from basic text searches to advanced
|
||||
query expressions and performance optimization.</p>
|
||||
<h2>Quick Start</h2>
|
||||
<p>New to Trilium search? Start here:</p>
|
||||
<ul>
|
||||
<li><strong><a href="#root/_help_WcwMZ2tDZmXK">Search Fundamentals</a></strong> -
|
||||
Basic concepts, syntax, and operators</li>
|
||||
</ul>
|
||||
<h2>Documentation Sections</h2>
|
||||
<h3>Core Search Features</h3>
|
||||
<ul>
|
||||
<li><strong><a href="#root/_help_WcwMZ2tDZmXK">Search Fundamentals</a></strong> -
|
||||
Basic search syntax, operators, and concepts</li>
|
||||
<li><strong><a href="#root/_help_ey9TMFyD8SHR">Advanced Search Expressions</a></strong> -
|
||||
Complex queries, boolean logic, and relationship traversal</li>
|
||||
</ul>
|
||||
<h3>Practical Applications</h3>
|
||||
<ul>
|
||||
<li><strong><a href="#root/_help_yAFfA1SAYlr7">Search Examples and Use Cases</a></strong> -
|
||||
Real-world examples for common workflows</li>
|
||||
<li><strong><a href="#root/_help_UUBStSxWzjgA">Saved Searches</a></strong> -
|
||||
Creating dynamic collections and dashboards</li>
|
||||
</ul>
|
||||
<h3>Technical Reference</h3>
|
||||
<ul>
|
||||
<li><strong><a href="#root/_help_ttFz520bcNLf">Technical Search Details</a></strong> -
|
||||
Performance, implementation, and optimization</li>
|
||||
</ul>
|
||||
<h2>Key Search Capabilities</h2>
|
||||
<h3>Full-Text Search</h3>
|
||||
<ul>
|
||||
<li>Search note titles and content</li>
|
||||
<li>Exact phrase matching with quotes</li>
|
||||
<li>Case-insensitive with diacritic normalization</li>
|
||||
<li>Support for multiple note types (text, code, mermaid, canvas)</li>
|
||||
</ul>
|
||||
<h3>Attribute-Based Search</h3>
|
||||
<ul>
|
||||
<li>Label searches: <code>#tag</code>, <code>#category=book</code>
|
||||
</li>
|
||||
<li>Relation searches: <code>~author</code>, <code>~author.title=Tolkien</code>
|
||||
</li>
|
||||
<li>Complex attribute combinations</li>
|
||||
<li>Fuzzy attribute matching</li>
|
||||
</ul>
|
||||
<h3>Property Search</h3>
|
||||
<ul>
|
||||
<li>Note metadata: <code>note.type=text</code>, <code>note.dateCreated >= TODAY-7</code>
|
||||
</li>
|
||||
<li>Hierarchical queries: <code>note.parents.title=Books</code>
|
||||
</li>
|
||||
<li>Relationship traversal: <code>note.children.labels.status=active</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Advanced Features</h3>
|
||||
<ul>
|
||||
<li><strong>Progressive Search</strong>: Exact matching first, fuzzy fallback
|
||||
when needed</li>
|
||||
<li><strong>Fuzzy Search</strong>: Typo tolerance and spelling variations</li>
|
||||
<li><strong>Boolean Logic</strong>: Complex AND/OR/NOT combinations</li>
|
||||
<li><strong>Date Arithmetic</strong>: Dynamic date calculations (TODAY-30,
|
||||
YEAR+1)</li>
|
||||
<li><strong>Regular Expressions</strong>: Pattern matching with <code>%=</code> operator</li>
|
||||
<li><strong>Ordering and Limiting</strong>: Custom sort orders and result
|
||||
limits</li>
|
||||
</ul>
|
||||
<h2>Search Operators Quick Reference</h2>
|
||||
<h3>Text Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code> - Exact match</li>
|
||||
<li><code>!=</code> - Not equal</li>
|
||||
<li><code>*=*</code> - Contains</li>
|
||||
<li><code>=*</code> - Starts with</li>
|
||||
<li><code>*=</code> - Ends with</li>
|
||||
<li><code>%=</code> - Regular expression</li>
|
||||
<li><code>~=</code> - Fuzzy exact match</li>
|
||||
<li><code>~*</code> - Fuzzy contains match</li>
|
||||
</ul>
|
||||
<h3>Numeric Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code>, <code>!=</code>, <code>></code>, <code>>=</code>, <code><</code>, <code><=</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Boolean Operators</h3>
|
||||
<ul>
|
||||
<li><code>AND</code>, <code>OR</code>, <code>NOT</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Special Syntax</h3>
|
||||
<ul>
|
||||
<li><code>#labelName</code> - Label exists</li>
|
||||
<li><code>#labelName=value</code> - Label equals value</li>
|
||||
<li><code>~relationName</code> - Relation exists</li>
|
||||
<li><code>~relationName.property</code> - Relation target property</li>
|
||||
<li><code>note.property</code> - Note property access</li>
|
||||
<li><code>"exact phrase"</code> - Quoted phrase search</li>
|
||||
</ul>
|
||||
<h2>Common Search Patterns</h2>
|
||||
<h3>Simple Searches</h3><pre><code class="language-text-x-trilium-auto">hello world # Find notes containing both words
|
||||
"project management" # Find exact phrase
|
||||
#task # Find notes with "task" label
|
||||
~author # Find notes with "author" relation</code></pre>
|
||||
<h3>Attribute Searches</h3><pre><code class="language-text-x-trilium-auto">#book #author=Tolkien # Books by Tolkien
|
||||
#task #priority=high #status!=completed # High-priority incomplete tasks
|
||||
~project.title *=* alpha # Notes related to projects with "alpha" in title</code></pre>
|
||||
<h3>Date-Based Searches</h3><pre><code class="language-text-x-trilium-auto">note.dateCreated >= TODAY-7 # Notes created in last week
|
||||
#dueDate <= TODAY+30 # Items due in next 30 days
|
||||
#eventDate = YEAR # Events scheduled for this year</code></pre>
|
||||
<h3>Complex Queries</h3><pre><code class="language-text-x-trilium-auto">(#book OR #article) AND #topic=programming AND note.dateModified >= MONTH
|
||||
#project AND (#status=active OR #status=pending) AND not(note.isArchived=true)</code></pre>
|
||||
<h2>Getting Started Checklist</h2>
|
||||
<ol>
|
||||
<li><strong>Learn Basic Syntax</strong> - Start with simple text and tag searches</li>
|
||||
<li><strong>Understand Operators</strong> - Master the core operators (<code>=</code>, <code>*=*</code>,
|
||||
etc.)</li>
|
||||
<li><strong>Practice Attributes</strong> - Use <code>#</code> for labels and <code>~</code> for
|
||||
relations</li>
|
||||
<li><strong>Try Boolean Logic</strong> - Combine searches with AND/OR/NOT</li>
|
||||
<li><strong>Explore Properties</strong> - Use <code>note.</code> prefix for metadata
|
||||
searches</li>
|
||||
<li><strong>Create Saved Searches</strong> - Turn useful queries into dynamic
|
||||
collections</li>
|
||||
<li><strong>Optimize Performance</strong> - Learn about fast search and limits</li>
|
||||
</ol>
|
||||
<h2>Performance Tips</h2>
|
||||
<ul>
|
||||
<li><strong>Use Fast Search</strong> for attribute-only queries</li>
|
||||
<li><strong>Set Reasonable Limits</strong> to prevent large result sets</li>
|
||||
<li><strong>Start Specific</strong> with the most selective criteria first</li>
|
||||
<li><strong>Leverage Attributes</strong> instead of content search when possible</li>
|
||||
<li><strong>Cache Common Queries</strong> as saved searches</li>
|
||||
</ul>
|
||||
<h2>Need Help?</h2>
|
||||
<ul>
|
||||
<li><strong>Examples</strong>: Check <a href="#root/_help_yAFfA1SAYlr7">Search Examples and Use Cases</a> for
|
||||
practical patterns</li>
|
||||
<li><strong>Complex Queries</strong>: See <a href="#root/_help_ey9TMFyD8SHR">Advanced Search Expressions</a> for
|
||||
sophisticated techniques</li>
|
||||
<li><strong>Performance Issues</strong>: Review <a href="#root/_help_ttFz520bcNLf">Technical Search Details</a> for
|
||||
optimization</li>
|
||||
<li><strong>Dynamic Collections</strong>: Learn about <a href="#root/_help_UUBStSxWzjgA">Saved Searches</a> for
|
||||
automated organization</li>
|
||||
</ul>
|
||||
<h2>Search Workflow Integration</h2>
|
||||
<p>Trilium's search integrates seamlessly with your note-taking workflow:</p>
|
||||
<ul>
|
||||
<li><strong>Quick Search</strong> (Ctrl+S) for instant access</li>
|
||||
<li><strong>Saved Searches</strong> for dynamic organization</li>
|
||||
<li><strong>Search from Subtree</strong> for focused queries</li>
|
||||
<li><strong>Auto-complete</strong> suggestions in search dialogs</li>
|
||||
<li><strong>URL-triggered searches</strong> for bookmarkable queries</li>
|
||||
</ul>
|
||||
<p>Start with the fundamentals and gradually explore advanced features as
|
||||
your needs grow. Trilium's search system is designed to scale from simple
|
||||
text queries to sophisticated knowledge management systems.</p>
|
||||
<p>Happy searching! 🔍</p>
|
||||
293
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html
generated
vendored
Normal file
293
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html
generated
vendored
Normal file
@@ -0,0 +1,293 @@
|
||||
<h2>Saved Searches</h2>
|
||||
<p>Saved searches in Trilium allow you to create dynamic collections of notes
|
||||
that automatically update based on search criteria. They appear as special
|
||||
notes in your tree and provide a powerful way to organize and access related
|
||||
content.</p>
|
||||
<h2>Understanding Saved Searches</h2>
|
||||
<p>A saved search is a special note type that:</p>
|
||||
<ul>
|
||||
<li>Stores search criteria and configuration</li>
|
||||
<li>Dynamically displays matching notes as children</li>
|
||||
<li>Updates automatically when notes change</li>
|
||||
<li>Can be bookmarked and accessed like any other note</li>
|
||||
<li>Supports all search features including ordering and limits</li>
|
||||
</ul>
|
||||
<h2>Creating Saved Searches</h2>
|
||||
<h3>From Search Dialog</h3>
|
||||
<ol>
|
||||
<li>Open the search dialog (Ctrl+S or search icon)</li>
|
||||
<li>Configure your search criteria and options</li>
|
||||
<li>Click "Save to note" button</li>
|
||||
<li>Choose a name and location for the saved search</li>
|
||||
</ol>
|
||||
<h3>Manual Creation</h3>
|
||||
<ol>
|
||||
<li>Create a new note and set its type to "Saved Search"</li>
|
||||
<li>Configure the search using labels:
|
||||
<ul>
|
||||
<li><code>#searchString</code> - The search query</li>
|
||||
<li><code>#fastSearch</code> - Enable fast search mode</li>
|
||||
<li><code>#includeArchivedNotes</code> - Include archived notes</li>
|
||||
<li><code>#orderBy</code> - Sort field</li>
|
||||
<li><code>#orderDirection</code> - "asc" or "desc"</li>
|
||||
<li><code>#limit</code> - Maximum number of results</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>Using Search Scripts</h3>
|
||||
<p>For complex logic, create a JavaScript note and link it:</p>
|
||||
<ul>
|
||||
<li><code>~searchScript</code> - Relation pointing to a backend script note</li>
|
||||
</ul>
|
||||
<h2>Basic Saved Search Examples</h2>
|
||||
<h3>Simple Text Search</h3><pre><code class="language-text-x-trilium-auto">#searchString=project management</code></pre>
|
||||
<p>Finds all notes containing "project management".</p>
|
||||
<h3>Tag-Based Collection</h3><pre><code class="language-text-x-trilium-auto">#searchString=#book #author=Tolkien
|
||||
#orderBy=publicationYear
|
||||
#orderDirection=desc</code></pre>
|
||||
<p>Creates a collection of Tolkien's books ordered by publication year.</p>
|
||||
<h3>Task Dashboard</h3><pre><code class="language-text-x-trilium-auto">#searchString=#task #status!=completed #assignee=me
|
||||
#orderBy=priority
|
||||
#orderDirection=desc
|
||||
#limit=20</code></pre>
|
||||
<p>Shows your top 20 incomplete tasks by priority.</p>
|
||||
<h3>Recent Activity</h3><pre><code class="language-text-x-trilium-auto">#searchString=note.dateModified >= TODAY-7
|
||||
#orderBy=dateModified
|
||||
#orderDirection=desc
|
||||
#limit=50</code></pre>
|
||||
<p>Shows the 50 most recently modified notes from the last week.</p>
|
||||
<h2>Advanced Saved Search Patterns</h2>
|
||||
<h3>Dynamic Date-Based Collections</h3>
|
||||
<h4>This Week's Content</h4><pre><code class="language-text-x-trilium-auto">#searchString=note.dateCreated >= TODAY-7 note.dateCreated < TODAY
|
||||
#orderBy=dateCreated
|
||||
#orderDirection=desc</code></pre>
|
||||
<h4>Monthly Review Collection</h4><pre><code class="language-text-x-trilium-auto">#searchString=#reviewed=false note.dateCreated >= MONTH note.dateCreated < MONTH+1
|
||||
#orderBy=dateCreated</code></pre>
|
||||
<h4>Upcoming Deadlines</h4><pre><code class="language-text-x-trilium-auto">#searchString=#dueDate >= TODAY #dueDate <= TODAY+14 #status!=completed
|
||||
#orderBy=dueDate
|
||||
#orderDirection=asc</code></pre>
|
||||
<h3>Project-Specific Collections</h3>
|
||||
<h4>Project Dashboard</h4><pre><code class="language-text-x-trilium-auto">#searchString=#project=alpha (#task OR #milestone OR #document)
|
||||
#orderBy=priority
|
||||
#orderDirection=desc</code></pre>
|
||||
<h4>Project Health Monitor</h4><pre><code class="language-text-x-trilium-auto">#searchString=#project=alpha #status=blocked OR (#dueDate < TODAY #status!=completed)
|
||||
#orderBy=dueDate
|
||||
#orderDirection=asc</code></pre>
|
||||
<h3>Content Type Collections</h3>
|
||||
<h4>Documentation Hub</h4><pre><code class="language-text-x-trilium-auto">#searchString=(#documentation OR #guide OR #manual) #product=api
|
||||
#orderBy=dateModified
|
||||
#orderDirection=desc</code></pre>
|
||||
<h4>Learning Path</h4><pre><code class="language-text-x-trilium-auto">#searchString=#course #level=beginner #topic=programming
|
||||
#orderBy=difficulty
|
||||
#orderDirection=asc</code></pre>
|
||||
<h2>Search Script Examples</h2>
|
||||
<p>For complex logic that can't be expressed in search strings, use JavaScript:</p>
|
||||
<h3>Custom Business Logic</h3><pre><code class="language-application-javascript-env-backend">// Find notes that need attention based on complex criteria
|
||||
const api = require('api');
|
||||
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 30);
|
||||
|
||||
const results = [];
|
||||
|
||||
// Find high-priority tasks overdue by more than a week
|
||||
const overdueTasks = api.searchForNotes(`
|
||||
#task #priority=high #dueDate < TODAY-7 #status!=completed
|
||||
`);
|
||||
|
||||
// Find projects with no recent activity
|
||||
const staleProjets = api.searchForNotes(`
|
||||
#project #status=active note.dateModified < TODAY-30
|
||||
`);
|
||||
|
||||
// Find notes with many attributes but no content
|
||||
const overlabeledNotes = api.searchForNotes(`
|
||||
note.attributeCount > 5 note.contentSize < 100
|
||||
`);
|
||||
|
||||
return [...overdueTasks, ...staleProjects, ...overlabeledNotes]
|
||||
.map(note => note.noteId);</code></pre>
|
||||
<h3>Dynamic Tag-Based Grouping</h3><pre><code class="language-application-javascript-env-backend">// Group notes by quarter based on creation date
|
||||
const api = require('api');
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const results = [];
|
||||
|
||||
for (let quarter = 1; quarter <= 4; quarter++) {
|
||||
const startMonth = (quarter - 1) * 3 + 1;
|
||||
const endMonth = quarter * 3;
|
||||
|
||||
const quarterNotes = api.searchForNotes(`
|
||||
note.dateCreated >= "${currentYear}-${String(startMonth).padStart(2, '0')}-01"
|
||||
note.dateCreated < "${currentYear}-${String(endMonth + 1).padStart(2, '0')}-01"
|
||||
#project
|
||||
`);
|
||||
|
||||
results.push(...quarterNotes.map(note => note.noteId));
|
||||
}
|
||||
|
||||
return results;</code></pre>
|
||||
<h3>Conditional Search Logic</h3><pre><code class="language-application-javascript-env-backend">// Smart dashboard that changes based on day of week
|
||||
const api = require('api');
|
||||
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||
|
||||
let searchQuery;
|
||||
|
||||
if (dayOfWeek === 1) { // Monday - weekly planning
|
||||
searchQuery = '#task #status=planned #week=' + getWeekNumber(today);
|
||||
} else if (dayOfWeek === 5) { // Friday - weekly review
|
||||
searchQuery = '#task #completed=true #week=' + getWeekNumber(today);
|
||||
} else { // Regular days - focus on today's work
|
||||
searchQuery = '#task #dueDate=TODAY #status!=completed';
|
||||
}
|
||||
|
||||
const notes = api.searchForNotes(searchQuery);
|
||||
return notes.map(note => note.noteId);
|
||||
|
||||
function getWeekNumber(date) {
|
||||
const firstDay = new Date(date.getFullYear(), 0, 1);
|
||||
const pastDays = Math.floor((date - firstDay) / 86400000);
|
||||
return Math.ceil((pastDays + firstDay.getDay() + 1) / 7);
|
||||
}</code></pre>
|
||||
<h2>Performance Optimization</h2>
|
||||
<h3>Fast Search for Large Collections</h3>
|
||||
<p>For collections that don't need content search:</p><pre><code class="language-text-x-trilium-auto">#searchString=#category=reference #type=article
|
||||
#fastSearch=true
|
||||
#limit=100</code></pre>
|
||||
<h3>Efficient Ordering</h3>
|
||||
<p>Use indexed properties for better performance:</p><pre><code class="language-text-x-trilium-auto">#orderBy=dateCreated
|
||||
#orderBy=title
|
||||
#orderBy=noteId</code></pre>
|
||||
<p>Avoid complex calculated orderings in large collections.</p>
|
||||
<h3>Result Limiting</h3>
|
||||
<p>Always set reasonable limits for large collections:</p><pre><code class="language-text-x-trilium-auto">#limit=50</code></pre>
|
||||
<p>For very large result sets, consider breaking into multiple saved searches.</p>
|
||||
<h2>Saved Search Organization</h2>
|
||||
<h3>Hierarchical Organization</h3>
|
||||
<p>Create a folder structure for saved searches:</p><pre><code class="language-text-x-trilium-auto">📁 Searches
|
||||
├── 📁 Projects
|
||||
│ ├── 🔍 Active Projects
|
||||
│ ├── 🔍 Overdue Tasks
|
||||
│ └── 🔍 Project Archive
|
||||
├── 📁 Content
|
||||
│ ├── 🔍 Recent Drafts
|
||||
│ ├── 🔍 Published Articles
|
||||
│ └── 🔍 Review Queue
|
||||
└── 📁 Maintenance
|
||||
├── 🔍 Untagged Notes
|
||||
├── 🔍 Cleanup Candidates
|
||||
└── 🔍 Orphaned Notes</code></pre>
|
||||
<h3>Search Naming Conventions</h3>
|
||||
<p>Use clear, descriptive names:</p>
|
||||
<ul>
|
||||
<li>"Active High-Priority Tasks"</li>
|
||||
<li>"This Month's Meeting Notes"</li>
|
||||
<li>"Unprocessed Inbox Items"</li>
|
||||
<li>"Literature Review Papers"</li>
|
||||
</ul>
|
||||
<h3>Search Labels</h3>
|
||||
<p>Tag saved searches for organization:</p><pre><code class="language-text-x-trilium-auto">#searchType=dashboard
|
||||
#searchType=maintenance
|
||||
#searchType=archive
|
||||
#frequency=daily
|
||||
#frequency=weekly</code></pre>
|
||||
<h2>Dashboard Creation</h2>
|
||||
<h3>Personal Dashboard</h3>
|
||||
<p>Combine multiple saved searches in a parent note:</p><pre><code class="language-text-x-trilium-auto">📋 My Dashboard
|
||||
├── 🔍 Today's Tasks
|
||||
├── 🔍 Urgent Items
|
||||
├── 🔍 Recent Notes
|
||||
├── 🔍 Upcoming Deadlines
|
||||
└── 🔍 Weekly Review Items</code></pre>
|
||||
<h3>Project Dashboard</h3><pre><code class="language-text-x-trilium-auto">📋 Project Alpha Dashboard
|
||||
├── 🔍 Active Tasks
|
||||
├── 🔍 Blocked Items
|
||||
├── 🔍 Recent Updates
|
||||
├── 🔍 Milestones
|
||||
└── 🔍 Team Notes</code></pre>
|
||||
<h3>Content Dashboard</h3><pre><code class="language-text-x-trilium-auto">📋 Content Management
|
||||
├── 🔍 Draft Articles
|
||||
├── 🔍 Review Queue
|
||||
├── 🔍 Published This Month
|
||||
├── 🔍 High-Engagement Posts
|
||||
└── 🔍 Content Ideas</code></pre>
|
||||
<h2>Maintenance and Updates</h2>
|
||||
<h3>Regular Review</h3>
|
||||
<p>Periodically review saved searches for:</p>
|
||||
<ul>
|
||||
<li>Outdated search criteria</li>
|
||||
<li>Performance issues</li>
|
||||
<li>Unused collections</li>
|
||||
<li>Scope creep</li>
|
||||
</ul>
|
||||
<h3>Search Evolution</h3>
|
||||
<p>As your note-taking evolves, update searches:</p>
|
||||
<ul>
|
||||
<li>Add new tags to existing searches</li>
|
||||
<li>Refine criteria based on usage patterns</li>
|
||||
<li>Split large collections into smaller ones</li>
|
||||
<li>Merge rarely-used collections</li>
|
||||
</ul>
|
||||
<h3>Performance Monitoring</h3>
|
||||
<p>Watch for performance issues:</p>
|
||||
<ul>
|
||||
<li>Slow-loading saved searches</li>
|
||||
<li>Memory usage with large result sets</li>
|
||||
<li>Search timeout errors</li>
|
||||
</ul>
|
||||
<h2>Troubleshooting</h2>
|
||||
<h3>Common Issues</h3>
|
||||
<h4>Empty Results</h4>
|
||||
<ul>
|
||||
<li>Check search syntax</li>
|
||||
<li>Verify tag spellings</li>
|
||||
<li>Ensure notes have required attributes</li>
|
||||
<li>Test search components individually</li>
|
||||
</ul>
|
||||
<h4>Performance Problems</h4>
|
||||
<ul>
|
||||
<li>Add <code>#fastSearch=true</code> for attribute-only searches</li>
|
||||
<li>Reduce result limits</li>
|
||||
<li>Simplify complex criteria</li>
|
||||
<li>Use indexed properties for ordering</li>
|
||||
</ul>
|
||||
<h4>Unexpected Results</h4>
|
||||
<ul>
|
||||
<li>Enable debug mode to see query parsing</li>
|
||||
<li>Test search in search dialog first</li>
|
||||
<li>Check for case sensitivity issues</li>
|
||||
<li>Verify date formats and ranges</li>
|
||||
</ul>
|
||||
<h3>Best Practices</h3>
|
||||
<h4>Search Design</h4>
|
||||
<ul>
|
||||
<li>Start simple and add complexity gradually</li>
|
||||
<li>Test searches thoroughly before saving</li>
|
||||
<li>Document complex search logic</li>
|
||||
<li>Use meaningful names and descriptions</li>
|
||||
</ul>
|
||||
<h4>Performance</h4>
|
||||
<ul>
|
||||
<li>Set appropriate limits</li>
|
||||
<li>Use fast search when possible</li>
|
||||
<li>Avoid overly complex expressions</li>
|
||||
<li>Monitor search execution time</li>
|
||||
</ul>
|
||||
<h4>Organization</h4>
|
||||
<ul>
|
||||
<li>Group related searches</li>
|
||||
<li>Use consistent naming conventions</li>
|
||||
<li>Archive unused searches</li>
|
||||
<li>Regular cleanup and maintenance</li>
|
||||
</ul>
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a href="#root/_help_ttFz520bcNLf">Technical Search Details</a> - Understanding
|
||||
search performance and implementation</li>
|
||||
<li><a href="#root/_help_yAFfA1SAYlr7">Search Examples and Use Cases</a> -
|
||||
More practical examples</li>
|
||||
<li><a href="#root/_help_ey9TMFyD8SHR">Advanced Search Expressions</a> - Complex
|
||||
query construction</li>
|
||||
</ul>
|
||||
152
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.html
generated
vendored
Normal file
152
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.html
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
<h2>Search Examples and Use Cases</h2>
|
||||
<p>This guide provides practical examples of how to use Trilium's search
|
||||
capabilities for common organizational patterns and workflows.</p>
|
||||
<h2>Personal Knowledge Management</h2>
|
||||
<h3>Research and Learning</h3>
|
||||
<p>Track your learning progress and find related materials:</p><pre><code class="language-text-x-trilium-auto">#topic=javascript #status=learning</code></pre>
|
||||
<p>Find all JavaScript materials you're currently learning.</p><pre><code class="language-text-x-trilium-auto">#course #completed=false note.dateCreated >= MONTH-1</code></pre>
|
||||
<p>Find courses started in the last month that aren't completed.</p><pre><code class="language-text-x-trilium-auto">#book #topic *=* programming #rating >= 4</code></pre>
|
||||
<p>Find highly-rated programming books.</p><pre><code class="language-text-x-trilium-auto">#paper ~author.title *=* "Andrew Ng" #field=machine-learning</code></pre>
|
||||
<p>Find machine learning papers by Andrew Ng.</p>
|
||||
<h3>Meeting and Event Management</h3>
|
||||
<p>Organize meetings, notes, and follow-ups:</p><pre><code class="language-text-x-trilium-auto">#meeting note.dateCreated >= TODAY-7 #attendee *=* smith</code></pre>
|
||||
<p>Find this week's meetings with Smith.</p><pre><code class="language-text-x-trilium-auto">#meeting #actionItems #status!=completed</code></pre>
|
||||
<p>Find meetings with outstanding action items.</p><pre><code class="language-text-x-trilium-auto">#event #date >= TODAY #date <= TODAY+30</code></pre>
|
||||
<p>Find upcoming events in the next 30 days.</p><pre><code class="language-text-x-trilium-auto">#meeting #project=alpha note.dateCreated >= MONTH</code></pre>
|
||||
<p>Find this month's meetings about project alpha.</p>
|
||||
<h3>Note Organization and Cleanup</h3>
|
||||
<p>Maintain and organize your note structure:</p><pre><code class="language-text-x-trilium-auto">note.childrenCount = 0 note.parentCount = 1 note.contentSize < 50 note.dateModified < TODAY-180</code></pre>
|
||||
<p>Find small, isolated notes not modified in 6 months (cleanup candidates).</p><pre><code class="language-text-x-trilium-auto">note.attributeCount = 0 note.type=text note.contentSize > 1000</code></pre>
|
||||
<p>Find large text notes without any labels (might need categorization).</p><pre><code class="language-text-x-trilium-auto">#draft note.dateCreated < TODAY-30</code></pre>
|
||||
<p>Find old draft notes that might need attention.</p><pre><code class="language-text-x-trilium-auto">note.parentCount > 3 note.type=text</code></pre>
|
||||
<p>Find notes that are heavily cloned (might indicate important content).</p>
|
||||
<h2>Project Management</h2>
|
||||
<h3>Task Tracking</h3>
|
||||
<p>Manage tasks and project progress:</p><pre><code class="language-text-x-trilium-auto">#task #priority=high #status!=completed #assignee=me</code></pre>
|
||||
<p>Find your high-priority incomplete tasks.</p><pre><code class="language-text-x-trilium-auto">#task #dueDate <= TODAY+3 #dueDate >= TODAY #status!=completed</code></pre>
|
||||
<p>Find tasks due in the next 3 days.</p><pre><code class="language-text-x-trilium-auto">#project=website #task #status=blocked</code></pre>
|
||||
<p>Find blocked tasks in the website project.</p><pre><code class="language-text-x-trilium-auto">#task #estimatedHours > 0 #actualHours > 0 orderBy note.dateModified desc</code></pre>
|
||||
<p>Find tasks with time tracking data, sorted by recent updates.</p>
|
||||
<h3>Project Oversight</h3>
|
||||
<p>Monitor project health and progress:</p><pre><code class="language-text-x-trilium-auto">#project #status=active note.children.labels.status = blocked</code></pre>
|
||||
<p>Find active projects with blocked tasks.</p><pre><code class="language-text-x-trilium-auto">#project #startDate <= TODAY-90 #status!=completed</code></pre>
|
||||
<p>Find projects that started over 90 days ago but aren't completed.</p><pre><code class="language-text-x-trilium-auto">#milestone #targetDate <= TODAY #status!=achieved</code></pre>
|
||||
<p>Find overdue milestones.</p><pre><code class="language-text-x-trilium-auto">#project orderBy note.childrenCount desc limit 10</code></pre>
|
||||
<p>Find the 10 largest projects by number of sub-notes.</p>
|
||||
<h3>Resource Planning</h3>
|
||||
<p>Track resources and dependencies:</p><pre><code class="language-text-x-trilium-auto">#resource #type=person #availability < 50</code></pre>
|
||||
<p>Find people with low availability.</p><pre><code class="language-text-x-trilium-auto">#dependency #status=pending #project=mobile-app</code></pre>
|
||||
<p>Find pending dependencies for the mobile app project.</p><pre><code class="language-text-x-trilium-auto">#budget #project #spent > #allocated</code></pre>
|
||||
<p>Find projects over budget.</p>
|
||||
<h2>Content Creation and Writing</h2>
|
||||
<h3>Writing Projects</h3>
|
||||
<p>Manage articles, books, and documentation:</p><pre><code class="language-text-x-trilium-auto">#article #status=draft #wordCount >= 1000</code></pre>
|
||||
<p>Find substantial draft articles.</p><pre><code class="language-text-x-trilium-auto">#chapter #book=novel #status=outline</code></pre>
|
||||
<p>Find novel chapters still in outline stage.</p><pre><code class="language-text-x-trilium-auto">#blog-post #published=false #topic=technology</code></pre>
|
||||
<p>Find unpublished technology blog posts.</p><pre><code class="language-text-x-trilium-auto">#documentation #lastReviewed < TODAY-90 #product=api</code></pre>
|
||||
<p>Find API documentation not reviewed in 90 days.</p>
|
||||
<h3>Editorial Workflow</h3>
|
||||
<p>Track editing and publication status:</p><pre><code class="language-text-x-trilium-auto">#article #editor=jane #status=review</code></pre>
|
||||
<p>Find articles assigned to Jane for review.</p><pre><code class="language-text-x-trilium-auto">#manuscript #submissionDate >= TODAY-30 #status=pending</code></pre>
|
||||
<p>Find manuscripts submitted in the last 30 days still pending.</p><pre><code class="language-text-x-trilium-auto">#publication #acceptanceDate >= YEAR #status=accepted</code></pre>
|
||||
<p>Find accepted publications this year.</p>
|
||||
<h3>Content Research</h3>
|
||||
<p>Organize research materials and sources:</p><pre><code class="language-text-x-trilium-auto">#source #reliability >= 8 #topic *=* climate</code></pre>
|
||||
<p>Find reliable sources about climate topics.</p><pre><code class="language-text-x-trilium-auto">#quote #author *=* Einstein #verified=true</code></pre>
|
||||
<p>Find verified Einstein quotes.</p><pre><code class="language-text-x-trilium-auto">#citation #used=false #relevance=high</code></pre>
|
||||
<p>Find high-relevance citations not yet used.</p>
|
||||
<h2>Business and Professional Use</h2>
|
||||
<h3>Client Management</h3>
|
||||
<p>Track client relationships and projects:</p><pre><code class="language-text-x-trilium-auto">#client=acme #project #status=active</code></pre>
|
||||
<p>Find active projects for ACME client.</p><pre><code class="language-text-x-trilium-auto">#meeting #client #date >= MONTH #followUp=required</code></pre>
|
||||
<p>Find client meetings this month requiring follow-up.</p><pre><code class="language-text-x-trilium-auto">#contract #renewalDate <= TODAY+60 #renewalDate >= TODAY</code></pre>
|
||||
<p>Find contracts expiring in the next 60 days.</p><pre><code class="language-text-x-trilium-auto">#invoice #status=unpaid #dueDate < TODAY</code></pre>
|
||||
<p>Find overdue unpaid invoices.</p>
|
||||
<h3>Process Documentation</h3>
|
||||
<p>Maintain procedures and workflows:</p><pre><code class="language-text-x-trilium-auto">#procedure #department=engineering #lastUpdated < TODAY-365</code></pre>
|
||||
<p>Find engineering procedures not updated in a year.</p><pre><code class="language-text-x-trilium-auto">#workflow #status=active #automation=possible</code></pre>
|
||||
<p>Find active workflows that could be automated.</p><pre><code class="language-text-x-trilium-auto">#checklist #process=onboarding #role=developer</code></pre>
|
||||
<p>Find onboarding checklists for developers.</p>
|
||||
<h3>Compliance and Auditing</h3>
|
||||
<p>Track compliance requirements and audits:</p><pre><code class="language-text-x-trilium-auto">#compliance #standard=sox #nextReview <= TODAY+30</code></pre>
|
||||
<p>Find SOX compliance items due for review soon.</p><pre><code class="language-text-x-trilium-auto">#audit #finding #severity=high #status!=resolved</code></pre>
|
||||
<p>Find unresolved high-severity audit findings.</p><pre><code class="language-text-x-trilium-auto">#policy #department=hr #effectiveDate >= YEAR</code></pre>
|
||||
<p>Find HR policies that became effective this year.</p>
|
||||
<h2>Academic and Educational Use</h2>
|
||||
<h3>Course Management</h3>
|
||||
<p>Organize courses and educational content:</p><pre><code class="language-text-x-trilium-auto">#course #semester=fall-2024 #assignment #dueDate >= TODAY</code></pre>
|
||||
<p>Find upcoming assignments for fall 2024 courses.</p><pre><code class="language-text-x-trilium-auto">#lecture #course=physics #topic *=* quantum</code></pre>
|
||||
<p>Find physics lectures about quantum topics.</p><pre><code class="language-text-x-trilium-auto">#student #grade < 70 #course=mathematics</code></pre>
|
||||
<p>Find students struggling in mathematics.</p><pre><code class="language-text-x-trilium-auto">#syllabus #course #lastUpdated < TODAY-180</code></pre>
|
||||
<p>Find syllabi not updated in 6 months.</p>
|
||||
<h3>Research Management</h3>
|
||||
<p>Track research projects and publications:</p><pre><code class="language-text-x-trilium-auto">#experiment #status=running #endDate <= TODAY+7</code></pre>
|
||||
<p>Find experiments ending in the next week.</p><pre><code class="language-text-x-trilium-auto">#dataset #size > 1000000 #cleaned=true #public=false</code></pre>
|
||||
<p>Find large, cleaned, private datasets.</p><pre><code class="language-text-x-trilium-auto">#hypothesis #tested=false #priority=high</code></pre>
|
||||
<p>Find high-priority untested hypotheses.</p><pre><code class="language-text-x-trilium-auto">#collaboration #institution *=* stanford #status=active</code></pre>
|
||||
<p>Find active collaborations with Stanford.</p>
|
||||
<h3>Grant and Funding</h3>
|
||||
<p>Manage funding applications and requirements:</p><pre><code class="language-text-x-trilium-auto">#grant #deadline <= TODAY+30 #deadline >= TODAY #status=in-progress</code></pre>
|
||||
<p>Find grant applications due in the next 30 days.</p><pre><code class="language-text-x-trilium-auto">#funding #amount >= 100000 #status=awarded #startDate >= YEAR</code></pre>
|
||||
<p>Find large grants awarded this year.</p><pre><code class="language-text-x-trilium-auto">#report #funding #dueDate <= TODAY+14 #status!=submitted</code></pre>
|
||||
<p>Find funding reports due in 2 weeks.</p>
|
||||
<h2>Technical Documentation</h2>
|
||||
<h3>Code and Development</h3>
|
||||
<p>Track code-related notes and documentation:</p><pre><code class="language-text-x-trilium-auto">#bug #severity=critical #status!=fixed #product=webapp</code></pre>
|
||||
<p>Find critical unfixed bugs in the web app.</p><pre><code class="language-text-x-trilium-auto">#feature #version=2.0 #status=implemented #tested=false</code></pre>
|
||||
<p>Find version 2.0 features that are implemented but not tested.</p><pre><code class="language-text-x-trilium-auto">#api #endpoint #deprecated=true #removalDate <= TODAY+90</code></pre>
|
||||
<p>Find deprecated API endpoints scheduled for removal soon.</p><pre><code class="language-text-x-trilium-auto">#architecture #component=database #lastReviewed < TODAY-180</code></pre>
|
||||
<p>Find database architecture documentation not reviewed in 6 months.</p>
|
||||
<h3>System Administration</h3>
|
||||
<p>Manage infrastructure and operations:</p><pre><code class="language-text-x-trilium-auto">#server #status=maintenance #scheduledDate >= TODAY #scheduledDate <= TODAY+7</code></pre>
|
||||
<p>Find servers scheduled for maintenance this week.</p><pre><code class="language-text-x-trilium-auto">#backup #status=failed #date >= TODAY-7</code></pre>
|
||||
<p>Find backup failures in the last week.</p><pre><code class="language-text-x-trilium-auto">#security #vulnerability #severity=high #patched=false</code></pre>
|
||||
<p>Find unpatched high-severity vulnerabilities.</p><pre><code class="language-text-x-trilium-auto">#monitoring #alert #frequency > 10 #period=week</code></pre>
|
||||
<p>Find alerts triggering more than 10 times per week.</p>
|
||||
<h2>Data Analysis and Reporting</h2>
|
||||
<h3>Performance Tracking</h3>
|
||||
<p>Monitor metrics and KPIs:</p><pre><code class="language-text-x-trilium-auto">#metric #kpi=true #trend=declining #period=month</code></pre>
|
||||
<p>Find declining monthly KPIs.</p><pre><code class="language-text-x-trilium-auto">#report #frequency=weekly #lastGenerated < TODAY-10</code></pre>
|
||||
<p>Find weekly reports that haven't been generated in 10 days.</p><pre><code class="language-text-x-trilium-auto">#dashboard #stakeholder=executive #lastUpdated < TODAY-7</code></pre>
|
||||
<p>Find executive dashboards not updated this week.</p>
|
||||
<h3>Trend Analysis</h3>
|
||||
<p>Track patterns and changes over time:</p><pre><code class="language-text-x-trilium-auto">#data #source=sales #period=quarter #analyzed=false</code></pre>
|
||||
<p>Find unanalyzed quarterly sales data.</p><pre><code class="language-text-x-trilium-auto">#trend #direction=up #significance=high #period=month</code></pre>
|
||||
<p>Find significant positive monthly trends.</p><pre><code class="language-text-x-trilium-auto">#forecast #accuracy < 80 #model=linear #period=quarter</code></pre>
|
||||
<p>Find inaccurate quarterly linear forecasts.</p>
|
||||
<h2>Search Strategy Tips</h2>
|
||||
<h3>Building Effective Queries</h3>
|
||||
<ol>
|
||||
<li><strong>Start Specific</strong>: Begin with the most selective criteria</li>
|
||||
<li><strong>Add Gradually</strong>: Build complexity incrementally</li>
|
||||
<li><strong>Test Components</strong>: Verify each part of complex queries</li>
|
||||
<li><strong>Use Shortcuts</strong>: Leverage <code>#</code> and <code>~</code> shortcuts
|
||||
for efficiency</li>
|
||||
</ol>
|
||||
<h3>Performance Optimization</h3>
|
||||
<ol>
|
||||
<li><strong>Use Fast Search</strong>: For large databases, enable fast search
|
||||
when content isn't needed</li>
|
||||
<li><strong>Limit Results</strong>: Add limits to prevent overwhelming result
|
||||
sets</li>
|
||||
<li><strong>Order Strategically</strong>: Put the most useful results first</li>
|
||||
<li><strong>Cache Common Queries</strong>: Save frequently used searches</li>
|
||||
</ol>
|
||||
<h3>Maintenance Patterns</h3>
|
||||
<p>Regular queries for note maintenance:</p><pre><code class="language-text-x-trilium-auto"># Weekly cleanup check
|
||||
note.attributeCount = 0 note.type=text note.contentSize < 100 note.dateModified < TODAY-30
|
||||
|
||||
# Monthly project review
|
||||
#project #status=active note.dateModified < TODAY-30
|
||||
|
||||
# Quarterly archive review
|
||||
note.isArchived=false note.dateModified < TODAY-90 note.childrenCount = 0</code></pre>
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a href="#root/_help_UUBStSxWzjgA">Saved Searches</a> - Convert these examples
|
||||
into reusable saved searches</li>
|
||||
<li><a href="#root/_help_ttFz520bcNLf">Technical Search Details</a> - Understanding
|
||||
performance and implementation</li>
|
||||
<li><a href="#root/_help_WcwMZ2tDZmXK">Search Fundamentals</a> - Review basic
|
||||
concepts and syntax</li>
|
||||
</ul>
|
||||
153
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.html
generated
vendored
Normal file
153
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.html
generated
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
<h2>Search Fundamentals</h2>
|
||||
<p>Trilium's search system is a powerful tool for finding and organizing
|
||||
notes. It supports multiple search modes, from simple text queries to complex
|
||||
expressions using attributes, relationships, and note properties.</p>
|
||||
<h2>Search Types Overview</h2>
|
||||
<p>Trilium provides three main search approaches:</p>
|
||||
<ol>
|
||||
<li><strong>Full-text Search</strong> - Searches within note titles and content</li>
|
||||
<li><strong>Attribute Search</strong> - Searches based on labels and relations
|
||||
attached to notes</li>
|
||||
<li><strong>Property Search</strong> - Searches based on note metadata (type,
|
||||
creation date, etc.)</li>
|
||||
</ol>
|
||||
<p>These can be combined in powerful ways to create precise queries.</p>
|
||||
<h2>Basic Search Syntax</h2>
|
||||
<h3>Simple Text Search</h3><pre><code class="language-text-x-trilium-auto">hello world</code></pre>
|
||||
<p>Finds notes containing both "hello" and "world" anywhere in the title
|
||||
or content.</p>
|
||||
<h3>Quoted Text Search</h3><pre><code class="language-text-x-trilium-auto">"hello world"</code></pre>
|
||||
<p>Finds notes containing the exact phrase "hello world".</p>
|
||||
<h3>Attribute Search</h3><pre><code class="language-text-x-trilium-auto">#tag</code></pre>
|
||||
<p>Finds notes with the label "tag".</p><pre><code class="language-text-x-trilium-auto">#category=book</code></pre>
|
||||
<p>Finds notes with label "category" set to "book".</p>
|
||||
<h3>Relation Search</h3><pre><code class="language-text-x-trilium-auto">~author</code></pre>
|
||||
<p>Finds notes with a relation named "author".</p><pre><code class="language-text-x-trilium-auto">~author.title=Tolkien</code></pre>
|
||||
<p>Finds notes with an "author" relation pointing to a note titled "Tolkien".</p>
|
||||
<h2>Search Operators</h2>
|
||||
<h3>Text Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code> - Exact match</li>
|
||||
<li><code>!=</code> - Not equal</li>
|
||||
<li><code>*=*</code> - Contains (substring)</li>
|
||||
<li><code>=*</code> - Starts with</li>
|
||||
<li><code>*=</code> - Ends with</li>
|
||||
<li><code>%=</code> - Regular expression match</li>
|
||||
<li><code>~=</code> - Fuzzy exact match</li>
|
||||
<li><code>~*</code> - Fuzzy contains match</li>
|
||||
</ul>
|
||||
<h3>Numeric Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code> - Equal</li>
|
||||
<li><code>!=</code> - Not equal</li>
|
||||
<li><code>></code> - Greater than</li>
|
||||
<li><code>>=</code> - Greater than or equal</li>
|
||||
<li><code><</code> - Less than</li>
|
||||
<li><code><=</code> - Less than or equal</li>
|
||||
</ul>
|
||||
<h3>Boolean Operators</h3>
|
||||
<ul>
|
||||
<li><code>AND</code> - Both conditions must be true</li>
|
||||
<li><code>OR</code> - Either condition must be true</li>
|
||||
<li><code>NOT</code> or <code>not()</code> - Condition must be false</li>
|
||||
</ul>
|
||||
<h2>Search Context and Scope</h2>
|
||||
<h3>Search Scope</h3>
|
||||
<p>By default, search covers:</p>
|
||||
<ul>
|
||||
<li>Note titles</li>
|
||||
<li>Note content (for text-based note types)</li>
|
||||
<li>Label names and values</li>
|
||||
<li>Relation names</li>
|
||||
<li>Note properties</li>
|
||||
</ul>
|
||||
<h3>Fast Search Mode</h3>
|
||||
<p>When enabled, fast search:</p>
|
||||
<ul>
|
||||
<li>Searches only titles and attributes</li>
|
||||
<li>Skips note content</li>
|
||||
<li>Provides faster results for large databases</li>
|
||||
</ul>
|
||||
<h3>Archived Notes</h3>
|
||||
<ul>
|
||||
<li>Excluded by default</li>
|
||||
<li>Can be included with "Include archived" option</li>
|
||||
</ul>
|
||||
<h2>Case Sensitivity and Normalization</h2>
|
||||
<ul>
|
||||
<li>All searches are case-insensitive</li>
|
||||
<li>Diacritics are normalized ("café" matches "cafe")</li>
|
||||
<li>Unicode characters are properly handled</li>
|
||||
</ul>
|
||||
<h2>Performance Considerations</h2>
|
||||
<h3>Content Size Limits</h3>
|
||||
<ul>
|
||||
<li>Note content is limited to 10MB for search processing</li>
|
||||
<li>Larger notes are still searchable by title and attributes</li>
|
||||
</ul>
|
||||
<h3>Progressive Search Strategy</h3>
|
||||
<ol>
|
||||
<li><strong>Exact Search Phase</strong>: Fast exact matching (handles 90%+
|
||||
of searches)</li>
|
||||
<li><strong>Fuzzy Search Phase</strong>: Activated when exact search returns
|
||||
fewer than 5 high-quality results</li>
|
||||
<li><strong>Result Ordering</strong>: Exact matches always appear before fuzzy
|
||||
matches</li>
|
||||
</ol>
|
||||
<h3>Search Optimization Tips</h3>
|
||||
<ul>
|
||||
<li>Use specific terms rather than very common words</li>
|
||||
<li>Combine full-text with attribute searches for precision</li>
|
||||
<li>Use fast search for large databases when content search isn't needed</li>
|
||||
<li>Limit results when dealing with very large result sets</li>
|
||||
</ul>
|
||||
<h2>Special Characters and Escaping</h2>
|
||||
<h3>Reserved Characters</h3>
|
||||
<p>These characters have special meaning in search queries:</p>
|
||||
<ul>
|
||||
<li><code>#</code> - Label indicator</li>
|
||||
<li><code>~</code> - Relation indicator</li>
|
||||
<li><code>()</code> - Grouping</li>
|
||||
<li><code>"</code> <code>'</code> <code>`</code> - Quotes for exact phrases</li>
|
||||
</ul>
|
||||
<h3>Escaping Special Characters</h3>
|
||||
<p>Use backslash to search for literal special characters:</p><pre><code class="language-text-x-trilium-auto">\#hashtag</code></pre>
|
||||
<p>Searches for the literal text "#hashtag" instead of a label.</p>
|
||||
<p>Use quotes to include special characters in phrases:</p><pre><code class="language-text-x-trilium-auto">"note.txt file"</code></pre>
|
||||
<p>Searches for the exact phrase including the dot.</p>
|
||||
<h2>Date and Time Values</h2>
|
||||
<h3>Special Date Keywords</h3>
|
||||
<ul>
|
||||
<li><code>TODAY</code> - Current date</li>
|
||||
<li><code>NOW</code> - Current date and time</li>
|
||||
<li><code>MONTH</code> - Current month</li>
|
||||
<li><code>YEAR</code> - Current year</li>
|
||||
</ul>
|
||||
<h3>Date Arithmetic</h3><pre><code class="language-text-x-trilium-auto">#dateCreated >= TODAY-30</code></pre>
|
||||
<p>Finds notes created in the last 30 days.</p><pre><code class="language-text-x-trilium-auto">#eventDate = YEAR+1</code></pre>
|
||||
<p>Finds notes with eventDate set to next year.</p>
|
||||
<h2>Search Results and Scoring</h2>
|
||||
<h3>Result Ranking</h3>
|
||||
<p>Results are ordered by:</p>
|
||||
<ol>
|
||||
<li>Relevance score (based on term frequency and position)</li>
|
||||
<li>Note depth (closer to root ranks higher)</li>
|
||||
<li>Alphabetical order for ties</li>
|
||||
</ol>
|
||||
<h3>Progressive Search Behavior</h3>
|
||||
<ul>
|
||||
<li>Exact matches always rank before fuzzy matches</li>
|
||||
<li>High-quality exact matches prevent fuzzy search activation</li>
|
||||
<li>Fuzzy matches help find content with typos or variations</li>
|
||||
</ul>
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a href="#root/_help_ey9TMFyD8SHR">Advanced Search Expressions</a> - Complex
|
||||
queries and combinations</li>
|
||||
<li><a href="#root/_help_yAFfA1SAYlr7">Search Examples and Use Cases</a> -
|
||||
Practical applications</li>
|
||||
<li><a href="#root/_help_UUBStSxWzjgA">Saved Searches</a> - Creating dynamic
|
||||
collections</li>
|
||||
<li><a href="#root/_help_ttFz520bcNLf">Technical Search Details</a> - Under-the-hood
|
||||
implementation</li>
|
||||
</ul>
|
||||
486
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.html
generated
vendored
Normal file
486
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.html
generated
vendored
Normal file
@@ -0,0 +1,486 @@
|
||||
<h2>Technical Search Details</h2>
|
||||
<p>This guide provides technical information about Trilium's search implementation,
|
||||
performance characteristics, and optimization strategies for power users
|
||||
and administrators.</p>
|
||||
<h2>Search Architecture Overview</h2>
|
||||
<h3>Three-Layer Search System</h3>
|
||||
<p>Trilium's search operates across three cache layers:</p>
|
||||
<ol>
|
||||
<li><strong>Becca (Backend Cache)</strong>: Server-side entity cache containing
|
||||
notes, attributes, and relationships</li>
|
||||
<li><strong>Froca (Frontend Cache)</strong>: Client-side mirror providing
|
||||
fast UI updates</li>
|
||||
<li><strong>Database Layer</strong>: SQLite database with FTS (Full-Text Search)
|
||||
support</li>
|
||||
</ol>
|
||||
<h3>Search Processing Pipeline</h3>
|
||||
<ol>
|
||||
<li><strong>Lexical Analysis</strong>: Query parsing and tokenization</li>
|
||||
<li><strong>Expression Building</strong>: Converting tokens to executable
|
||||
expressions</li>
|
||||
<li><strong>Progressive Execution</strong>: Exact search followed by optional
|
||||
fuzzy search</li>
|
||||
<li><strong>Result Scoring</strong>: Relevance calculation and ranking</li>
|
||||
<li><strong>Result Presentation</strong>: Formatting and highlighting</li>
|
||||
</ol>
|
||||
<h2>Query Processing Details</h2>
|
||||
<h3>Lexical Analysis (Lex)</h3>
|
||||
<p>The lexer breaks down search queries into components:</p><pre><code class="language-application-javascript-env-backend">// Input: 'project #status=active note.dateCreated >= TODAY-7'
|
||||
// Output:
|
||||
{
|
||||
fulltextTokens: ['project'],
|
||||
expressionTokens: ['#status', '=', 'active', 'note', '.', 'dateCreated', '>=', 'TODAY-7']
|
||||
}</code></pre>
|
||||
<h4>Token Types</h4>
|
||||
<ul>
|
||||
<li><strong>Fulltext Tokens</strong>: Regular search terms</li>
|
||||
<li><strong>Expression Tokens</strong>: Attributes, operators, and property
|
||||
references</li>
|
||||
<li><strong>Quoted Strings</strong>: Exact phrase matches</li>
|
||||
<li><strong>Escaped Characters</strong>: Literal special characters</li>
|
||||
</ul>
|
||||
<h3>Expression Building (Parse)</h3>
|
||||
<p>Tokens are converted into executable expression trees:</p><pre><code class="language-application-javascript-env-backend">// Expression tree for: #book AND #author=Tolkien
|
||||
AndExp([
|
||||
AttributeExistsExp('label', 'book'),
|
||||
LabelComparisonExp('label', 'author', equals('tolkien'))
|
||||
])</code></pre>
|
||||
<h4>Expression Types</h4>
|
||||
<ul>
|
||||
<li><code>AndExp</code>, <code>OrExp</code>, <code>NotExp</code>: Boolean logic</li>
|
||||
<li><code>AttributeExistsExp</code>: Label/relation existence</li>
|
||||
<li><code>LabelComparisonExp</code>: Label value comparison</li>
|
||||
<li><code>RelationWhereExp</code>: Relation target queries</li>
|
||||
<li><code>PropertyComparisonExp</code>: Note property filtering</li>
|
||||
<li><code>NoteContentFulltextExp</code>: Content search</li>
|
||||
<li><code>OrderByAndLimitExp</code>: Result ordering and limiting</li>
|
||||
</ul>
|
||||
<h3>Progressive Search Strategy</h3>
|
||||
<h4>Phase 1: Exact Search</h4><pre><code class="language-application-javascript-env-backend">// Fast exact matching
|
||||
const exactResults = performSearch(expression, searchContext, false);</code></pre>
|
||||
<p>Characteristics:</p>
|
||||
<ul>
|
||||
<li>Substring matching for text</li>
|
||||
<li>Exact attribute matching</li>
|
||||
<li>Property-based filtering</li>
|
||||
<li>Handles 90%+ of searches</li>
|
||||
<li>Sub-second response time</li>
|
||||
</ul>
|
||||
<h4>Phase 2: Fuzzy Fallback</h4><pre><code class="language-application-javascript-env-backend">// Activated when exact results < 5 high-quality matches
|
||||
if (highQualityResults.length < 5) {
|
||||
const fuzzyResults = performSearch(expression, searchContext, true);
|
||||
return mergeExactAndFuzzyResults(exactResults, fuzzyResults);
|
||||
}</code></pre>
|
||||
<p>Characteristics:</p>
|
||||
<ul>
|
||||
<li>Edit distance calculations</li>
|
||||
<li>Phrase proximity matching</li>
|
||||
<li>Typo tolerance</li>
|
||||
<li>Performance safeguards</li>
|
||||
<li>Exact matches always rank first</li>
|
||||
</ul>
|
||||
<h2>Performance Characteristics</h2>
|
||||
<h3>Search Limits and Thresholds</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>MAX_SEARCH_CONTENT_SIZE</code>
|
||||
</td>
|
||||
<td>2MB</td>
|
||||
<td>Database-level content filtering</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MIN_FUZZY_TOKEN_LENGTH</code>
|
||||
</td>
|
||||
<td>3 chars</td>
|
||||
<td>Minimum length for fuzzy matching</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MAX_EDIT_DISTANCE</code>
|
||||
</td>
|
||||
<td>2 chars</td>
|
||||
<td>Maximum character changes for fuzzy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MAX_PHRASE_PROXIMITY</code>
|
||||
</td>
|
||||
<td>10 words</td>
|
||||
<td>Maximum distance for phrase matching</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>RESULT_SUFFICIENCY_THRESHOLD</code>
|
||||
</td>
|
||||
<td>5 results</td>
|
||||
<td>Threshold for fuzzy activation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ABSOLUTE_MAX_CONTENT_SIZE</code>
|
||||
</td>
|
||||
<td>100MB</td>
|
||||
<td>Hard limit to prevent system crash</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ABSOLUTE_MAX_WORD_COUNT</code>
|
||||
</td>
|
||||
<td>2M words</td>
|
||||
<td>Hard limit for word processing</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Performance Optimization</h3>
|
||||
<h4>Database-Level Optimizations</h4><pre><code class="language-text-x-mariadb">-- Content size filtering at database level
|
||||
SELECT noteId, type, mime, content, isProtected
|
||||
FROM notes JOIN blobs USING (blobId)
|
||||
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND isDeleted = 0
|
||||
AND LENGTH(content) < 2097152 -- 2MB limit</code></pre>
|
||||
<h4>Memory Management</h4>
|
||||
<ul>
|
||||
<li>Single-array edit distance calculation</li>
|
||||
<li>Early termination for distant matches</li>
|
||||
<li>Progressive content processing</li>
|
||||
<li>Cached regular expressions</li>
|
||||
</ul>
|
||||
<h4>Search Context Optimization</h4><pre><code class="language-application-javascript-env-backend">// Efficient search context configuration
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: true, // Skip content search
|
||||
limit: 50, // Reasonable result limit
|
||||
orderBy: 'dateCreated', // Use indexed property
|
||||
includeArchivedNotes: false // Reduce search space
|
||||
});</code></pre>
|
||||
<h2>Fuzzy Search Implementation</h2>
|
||||
<h3>Edit Distance Algorithm</h3>
|
||||
<p>Trilium uses an optimized Levenshtein distance calculation:</p><pre><code class="language-application-javascript-env-backend">// Optimized single-array implementation
|
||||
function calculateOptimizedEditDistance(str1, str2, maxDistance) {
|
||||
// Early termination checks
|
||||
if (Math.abs(str1.length - str2.length) > maxDistance) {
|
||||
return maxDistance + 1;
|
||||
}
|
||||
|
||||
// Single array optimization
|
||||
let previousRow = Array.from({ length: str2.length + 1 }, (_, i) => i);
|
||||
let currentRow = new Array(str2.length + 1);
|
||||
|
||||
for (let i = 1; i <= str1.length; i++) {
|
||||
currentRow[0] = i;
|
||||
let minInRow = i;
|
||||
|
||||
for (let j = 1; j <= str2.length; j++) {
|
||||
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
||||
currentRow[j] = Math.min(
|
||||
previousRow[j] + 1, // deletion
|
||||
currentRow[j - 1] + 1, // insertion
|
||||
previousRow[j - 1] + cost // substitution
|
||||
);
|
||||
minInRow = Math.min(minInRow, currentRow[j]);
|
||||
}
|
||||
|
||||
// Early termination if row minimum exceeds threshold
|
||||
if (minInRow > maxDistance) return maxDistance + 1;
|
||||
|
||||
[previousRow, currentRow] = [currentRow, previousRow];
|
||||
}
|
||||
|
||||
return previousRow[str2.length];
|
||||
}</code></pre>
|
||||
<h3>Phrase Proximity Matching</h3>
|
||||
<p>For multi-token fuzzy searches:</p><pre><code class="language-application-javascript-env-backend">// Check if tokens appear within reasonable proximity
|
||||
function hasProximityMatch(tokenPositions, maxDistance = 10) {
|
||||
// For 2 tokens, simple distance check
|
||||
if (tokenPositions.length === 2) {
|
||||
const [pos1, pos2] = tokenPositions;
|
||||
return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
|
||||
}
|
||||
|
||||
// For multiple tokens, find sequence within range
|
||||
const findSequence = (remaining, currentPos) => {
|
||||
if (remaining.length === 0) return true;
|
||||
const [nextPositions, ...rest] = remaining;
|
||||
return nextPositions.some(pos =>
|
||||
Math.abs(pos - currentPos) <= maxDistance &&
|
||||
findSequence(rest, pos)
|
||||
);
|
||||
};
|
||||
|
||||
const [firstPositions, ...rest] = tokenPositions;
|
||||
return firstPositions.some(startPos => findSequence(rest, startPos));
|
||||
}</code></pre>
|
||||
<h2>Indexing and Storage</h2>
|
||||
<h3>Database Schema Optimization</h3><pre><code class="language-text-x-mariadb">-- Relevant indexes for search performance
|
||||
CREATE INDEX idx_notes_type ON notes(type);
|
||||
CREATE INDEX idx_notes_isDeleted ON notes(isDeleted);
|
||||
CREATE INDEX idx_notes_dateCreated ON notes(dateCreated);
|
||||
CREATE INDEX idx_notes_dateModified ON notes(dateModified);
|
||||
CREATE INDEX idx_attributes_name ON attributes(name);
|
||||
CREATE INDEX idx_attributes_type ON attributes(type);
|
||||
CREATE INDEX idx_attributes_value ON attributes(value);</code></pre>
|
||||
<h3>Content Processing</h3>
|
||||
<p>Notes are processed differently based on type:</p><pre><code class="language-application-javascript-env-backend">// Content preprocessing by note type
|
||||
function preprocessContent(content, type, mime) {
|
||||
content = normalize(content.toString());
|
||||
|
||||
if (type === "text" && mime === "text/html") {
|
||||
content = stripTags(content);
|
||||
content = content.replace(/&nbsp;/g, " ");
|
||||
} else if (type === "mindMap" && mime === "application/json") {
|
||||
content = processMindmapContent(content);
|
||||
} else if (type === "canvas" && mime === "application/json") {
|
||||
const canvasData = JSON.parse(content);
|
||||
const textElements = canvasData.elements
|
||||
.filter(el => el.type === "text" && el.text)
|
||||
.map(el => el.text);
|
||||
content = normalize(textElements.join(" "));
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
}</code></pre>
|
||||
<h2>Search Result Processing</h2>
|
||||
<h3>Scoring Algorithm</h3>
|
||||
<p>Results are scored based on multiple factors:</p><pre><code class="language-application-javascript-env-backend">function computeScore(fulltextQuery, highlightedTokens, enableFuzzyMatching) {
|
||||
let score = 0;
|
||||
|
||||
// Title matches get higher score
|
||||
if (this.noteTitle.toLowerCase().includes(fulltextQuery.toLowerCase())) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Path matches (hierarchical context)
|
||||
const pathMatch = this.notePathArray.some(pathNote =>
|
||||
pathNote.title.toLowerCase().includes(fulltextQuery.toLowerCase())
|
||||
);
|
||||
if (pathMatch) score += 5;
|
||||
|
||||
// Attribute matches
|
||||
score += this.attributeMatches * 3;
|
||||
|
||||
// Content snippet quality
|
||||
if (this.contentSnippet && this.contentSnippet.length > 0) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
// Fuzzy match penalty
|
||||
if (enableFuzzyMatching && this.isFuzzyMatch) {
|
||||
score *= 0.8; // 20% penalty for fuzzy matches
|
||||
}
|
||||
|
||||
return score;
|
||||
}</code></pre>
|
||||
<h3>Result Merging</h3>
|
||||
<p>Exact and fuzzy results are carefully merged:</p><pre><code class="language-application-javascript-env-backend">function mergeExactAndFuzzyResults(exactResults, fuzzyResults) {
|
||||
// Deduplicate - exact results take precedence
|
||||
const exactNoteIds = new Set(exactResults.map(r => r.noteId));
|
||||
const additionalFuzzyResults = fuzzyResults.filter(r =>
|
||||
!exactNoteIds.has(r.noteId)
|
||||
);
|
||||
|
||||
// Sort within each category
|
||||
exactResults.sort(byScoreAndDepth);
|
||||
additionalFuzzyResults.sort(byScoreAndDepth);
|
||||
|
||||
// CRITICAL: Exact matches always come first
|
||||
return [...exactResults, ...additionalFuzzyResults];
|
||||
}</code></pre>
|
||||
<h2>Performance Monitoring</h2>
|
||||
<h3>Search Metrics</h3>
|
||||
<p>Monitor these performance indicators:</p><pre><code class="language-application-javascript-env-backend">// Performance tracking
|
||||
const searchMetrics = {
|
||||
totalQueries: 0,
|
||||
exactSearchTime: 0,
|
||||
fuzzySearchTime: 0,
|
||||
resultCount: 0,
|
||||
cacheHitRate: 0,
|
||||
slowQueries: [] // queries taking > 1 second
|
||||
};</code></pre>
|
||||
<h3>Memory Usage</h3>
|
||||
<p>Track memory consumption:</p><pre><code class="language-application-javascript-env-backend">// Memory monitoring
|
||||
const memoryMetrics = {
|
||||
searchCacheSize: 0,
|
||||
activeSearchContexts: 0,
|
||||
largeContentNotes: 0, // notes > 1MB
|
||||
indexSize: 0
|
||||
};</code></pre>
|
||||
<h3>Query Complexity Analysis</h3>
|
||||
<p>Identify expensive queries:</p><pre><code class="language-application-javascript-env-backend">// Query complexity factors
|
||||
const complexityFactors = {
|
||||
tokenCount: query.split(' ').length,
|
||||
hasRegex: query.includes('%='),
|
||||
hasFuzzy: query.includes('~=') || query.includes('~*'),
|
||||
hasRelationTraversal: query.includes('.relations.'),
|
||||
hasNestedProperties: (query.match(/\./g) || []).length > 2,
|
||||
hasOrderBy: query.includes('orderBy'),
|
||||
estimatedResultSize: 'unknown'
|
||||
};</code></pre>
|
||||
<h2>Troubleshooting Performance Issues</h2>
|
||||
<h3>Common Performance Problems</h3>
|
||||
<h4>Slow Full-Text Search</h4><pre><code class="language-application-javascript-env-backend">// Diagnosis
|
||||
- Check note content sizes
|
||||
- Verify content type filtering
|
||||
- Monitor regex usage
|
||||
- Review fuzzy search activation
|
||||
|
||||
// Solutions
|
||||
- Enable fast search for attribute-only queries
|
||||
- Add content size limits
|
||||
- Optimize regex patterns
|
||||
- Tune fuzzy search thresholds</code></pre>
|
||||
<h4>Memory Issues</h4><pre><code class="language-application-javascript-env-backend">// Diagnosis
|
||||
- Monitor result set sizes
|
||||
- Check for large content processing
|
||||
- Review search context caching
|
||||
- Identify memory leaks
|
||||
|
||||
// Solutions
|
||||
- Add result limits
|
||||
- Implement progressive loading
|
||||
- Clear unused search contexts
|
||||
- Optimize content preprocessing</code></pre>
|
||||
<h4>High CPU Usage</h4><pre><code class="language-application-javascript-env-backend">// Diagnosis
|
||||
- Profile fuzzy search operations
|
||||
- Check edit distance calculations
|
||||
- Monitor regex compilation
|
||||
- Review phrase proximity matching
|
||||
|
||||
// Solutions
|
||||
- Increase minimum fuzzy token length
|
||||
- Reduce maximum edit distance
|
||||
- Cache compiled regexes
|
||||
- Limit phrase proximity distance</code></pre>
|
||||
<h3>Debugging Tools</h3>
|
||||
<h4>Debug Mode</h4>
|
||||
<p>Enable search debugging:</p><pre><code class="language-application-javascript-env-backend">// Search context with debugging
|
||||
const searchContext = new SearchContext({
|
||||
debug: true // Logs expression parsing and execution
|
||||
});</code></pre>
|
||||
<p>Output includes:</p>
|
||||
<ul>
|
||||
<li>Token parsing results</li>
|
||||
<li>Expression tree structure</li>
|
||||
<li>Execution timing</li>
|
||||
<li>Result scoring details</li>
|
||||
</ul>
|
||||
<h4>Performance Profiling</h4><pre><code class="language-application-javascript-env-backend">// Manual performance measurement
|
||||
const startTime = Date.now();
|
||||
const results = searchService.findResultsWithQuery(query, searchContext);
|
||||
const endTime = Date.now();
|
||||
console.log(`Search took ${endTime - startTime}ms for ${results.length} results`);</code></pre>
|
||||
<h4>Query Analysis</h4><pre><code class="language-application-javascript-env-backend">// Analyze query complexity
|
||||
function analyzeQuery(query) {
|
||||
return {
|
||||
tokenCount: query.split(/\s+/).length,
|
||||
hasAttributes: /#|\~/.test(query),
|
||||
hasProperties: /note\./.test(query),
|
||||
hasRegex: /%=/.test(query),
|
||||
hasFuzzy: /~[=*]/.test(query),
|
||||
complexity: calculateComplexityScore(query)
|
||||
};
|
||||
}</code></pre>
|
||||
<h2>Configuration and Tuning</h2>
|
||||
<h3>Server Configuration</h3>
|
||||
<p>Relevant settings in <code>config.ini</code>:</p><pre><code class="language-text-x-toml"># Search-related settings
|
||||
[Search]
|
||||
maxContentSize=2097152 # 2MB content limit
|
||||
minFuzzyTokenLength=3 # Minimum chars for fuzzy
|
||||
maxEditDistance=2 # Edit distance limit
|
||||
resultSufficiencyThreshold=5 # Fuzzy activation threshold
|
||||
enableProgressiveSearch=true # Enable progressive strategy
|
||||
cacheSearchResults=true # Cache frequent searches
|
||||
|
||||
# Performance settings
|
||||
[Performance]
|
||||
searchTimeoutMs=30000 # 30 second search timeout
|
||||
maxSearchResults=1000 # Hard limit on results
|
||||
enableSearchProfiling=false # Performance logging</code></pre>
|
||||
<h3>Runtime Tuning</h3>
|
||||
<p>Adjust search behavior programmatically:</p><pre><code class="language-application-javascript-env-backend">// Dynamic configuration
|
||||
const searchConfig = {
|
||||
maxContentSize: 1024 * 1024, // 1MB for faster processing
|
||||
enableFuzzySearch: false, // Exact only for speed
|
||||
resultLimit: 50, // Smaller result sets
|
||||
useIndexedPropertiesOnly: true // Skip expensive calculations
|
||||
};</code></pre>
|
||||
<h2>Best Practices for Performance</h2>
|
||||
<h3>Query Design</h3>
|
||||
<ol>
|
||||
<li><strong>Start Specific</strong>: Use selective criteria first</li>
|
||||
<li><strong>Limit Results</strong>: Always set reasonable limits</li>
|
||||
<li><strong>Use Indexes</strong>: Prefer indexed properties for ordering</li>
|
||||
<li><strong>Avoid Regex</strong>: Use simple operators when possible</li>
|
||||
<li><strong>Cache Common Queries</strong>: Save frequently used searches</li>
|
||||
</ol>
|
||||
<h3>System Administration</h3>
|
||||
<ol>
|
||||
<li><strong>Monitor Performance</strong>: Track slow queries and memory usage</li>
|
||||
<li><strong>Regular Maintenance</strong>: Clean up unused notes and attributes</li>
|
||||
<li><strong>Index Optimization</strong>: Ensure database indexes are current</li>
|
||||
<li><strong>Content Management</strong>: Archive or compress large content</li>
|
||||
</ol>
|
||||
<h3>Development Guidelines</h3>
|
||||
<ol>
|
||||
<li><strong>Test Performance</strong>: Benchmark complex queries</li>
|
||||
<li><strong>Profile Regularly</strong>: Identify performance regressions</li>
|
||||
<li><strong>Optimize Incrementally</strong>: Make small, measured improvements</li>
|
||||
<li><strong>Document Complexity</strong>: Note expensive operations</li>
|
||||
</ol>
|
||||
<h2>Advanced Configuration</h2>
|
||||
<h3>Custom Search Extensions</h3>
|
||||
<p>Extend search functionality with custom expressions:</p><pre><code class="language-application-javascript-env-backend">// Custom expression example
|
||||
class CustomDateRangeExp extends Expression {
|
||||
constructor(dateField, startDate, endDate) {
|
||||
super();
|
||||
this.dateField = dateField;
|
||||
this.startDate = startDate;
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
execute(inputNoteSet, executionContext, searchContext) {
|
||||
// Custom logic for date range filtering
|
||||
// with optimized performance characteristics
|
||||
}
|
||||
}</code></pre>
|
||||
<h3>Search Result Caching</h3>
|
||||
<p>Implement result caching for frequent queries:</p><pre><code class="language-application-javascript-env-backend">// Simple LRU cache for search results
|
||||
class SearchResultCache {
|
||||
constructor(maxSize = 100) {
|
||||
this.cache = new Map();
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
get(queryKey) {
|
||||
if (this.cache.has(queryKey)) {
|
||||
// Move to end (most recently used)
|
||||
const value = this.cache.get(queryKey);
|
||||
this.cache.delete(queryKey);
|
||||
this.cache.set(queryKey, value);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(queryKey, results) {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
// Remove least recently used
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
this.cache.set(queryKey, results);
|
||||
}
|
||||
}</code></pre>
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a href="#root/_help_WcwMZ2tDZmXK">Search Fundamentals</a> - Basic concepts
|
||||
and syntax</li>
|
||||
<li><a href="#root/_help_ey9TMFyD8SHR">Advanced Search Expressions</a> - Complex
|
||||
query construction</li>
|
||||
<li><a href="#root/_help_yAFfA1SAYlr7">Search Examples and Use Cases</a> -
|
||||
Practical applications</li>
|
||||
<li><a href="#root/_help_UUBStSxWzjgA">Saved Searches</a> - Creating dynamic
|
||||
collections</li>
|
||||
</ul>
|
||||
61
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation.html
generated
vendored
Normal file
61
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation.html
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
<p>Running Trilium on a server lets you access your notes from any device
|
||||
through a web browser and enables synchronization between multiple Trilium
|
||||
instances. This guide covers the different ways to install and configure
|
||||
Trilium on your server.</p>
|
||||
<h2>Choose Your Installation Method</h2>
|
||||
<p>The easiest way to get started is with Docker, which works on most systems
|
||||
and architectures. If you prefer not to manage your own server, PikaPods
|
||||
offers managed hosting.</p>
|
||||
<p><strong>Recommended approaches:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="#root/_help_FNVzcT2FwEjq">Docker Installation</a> - Works on AMD64
|
||||
and ARM architectures</li>
|
||||
<li><a href="https://www.pikapods.com/pods?run=trilium-next">PikaPods managed hosting</a> -
|
||||
No server management required</li>
|
||||
<li><a href="#root/_help_Q5jqLlwsnIBb">Packaged Server Installation</a> - Native
|
||||
Linux packages</li>
|
||||
</ul>
|
||||
<p><strong>Advanced options:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="#root/_help_baqzSqQefEpE">Manual Installation</a> - Full control
|
||||
over the setup</li>
|
||||
<li><a href="#root/_help_9ErAobCLD1jl">Kubernetes</a> - For container orchestration</li>
|
||||
<li><a href="#root/_help_6YVZgQtfkxkS">NixOS Module</a> - Declarative configuration</li>
|
||||
</ul>
|
||||
<p>All server installations include both desktop and mobile web interfaces.</p>
|
||||
<h2>Configuration</h2>
|
||||
<p>Trilium stores its configuration in a <code>config.ini</code> file located
|
||||
in the <a href="#root/_help_dvbMBRXYMM2G">data directory</a>. To customize
|
||||
your installation, copy the sample configuration file and modify it:</p><pre><code class="language-text-x-sh">cp config-sample.ini config.ini</code></pre>
|
||||
<p>You can also use environment variables instead of the config file. This
|
||||
is particularly useful for Docker deployments. See the <a href="#root/_help_SneMubD5wTR6">configuration guide</a> for
|
||||
all available options.</p>
|
||||
<h3>Changing the Data Directory</h3>
|
||||
<p>To store Trilium's data (database, config, backups) in a custom location,
|
||||
set the <code>TRILIUM_DATA_DIR</code> environment variable:</p><pre><code class="language-text-x-sh">export TRILIUM_DATA_DIR=/path/to/your/trilium-data</code></pre>
|
||||
<h3>Upload Size Limits</h3>
|
||||
<p>By default, Trilium limits file uploads to 250MB. You can adjust this
|
||||
limit based on your needs:</p><pre><code class="language-text-x-sh"># Increase limit to 450MB
|
||||
export MAX_ALLOWED_FILE_SIZE_MB=450
|
||||
|
||||
# Remove limit entirely (use with caution)
|
||||
export TRILIUM_NO_UPLOAD_LIMIT=true</code></pre>
|
||||
<h3>Disabling Authentication</h3>
|
||||
<p>See <a class="reference-link" href="#root/_help_CpWc4lepON8y">Authentication</a>.</p>
|
||||
<h2>Reverse Proxy Setup</h2>
|
||||
<p>If you want to access Trilium through a domain name or alongside other
|
||||
web services, you'll need to configure a reverse proxy. Here's a basic
|
||||
nginx configuration:</p><pre><code class="language-text-x-nginx-conf">location /trilium/ {
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Allow larger file uploads (in server block)
|
||||
client_max_body_size 0; # 0 = unlimited</code></pre>
|
||||
<p>For Apache configuration, see the <a href="#root/_help_Eu2zkbyE2QPK">Apache proxy setup</a> guide.</p>
|
||||
@@ -0,0 +1,43 @@
|
||||
<aside class="admonition warning">
|
||||
<p>This page describes manually installing Trilium on your server. <strong>Note that this is a not well supported way to install Trilium, problems may appear, information laid out here is quite out of date. It is recommended to use either</strong>
|
||||
<a
|
||||
class="reference-link" href="#root/_help_FNVzcT2FwEjq">Docker Server Installation</a> <strong>or</strong> <a class="reference-link"
|
||||
href="#root/_help_Q5jqLlwsnIBb">Packaged server installation</a><strong>.</strong>
|
||||
</p>
|
||||
</aside>
|
||||
<h2>Requirements</h2>
|
||||
<p>Trilium is a node.js application. Supported (tested) version of node.js
|
||||
is latest 14.X.X and 16.X.X. Trilium might work with older versions as
|
||||
well.</p>
|
||||
<p>You can check your node version with this command (node.js needs to be
|
||||
installed):</p><pre><code class="language-text-x-trilium-auto">node --version</code></pre>
|
||||
<p>If your Linux distribution has only an outdated version of node.js, you
|
||||
can take a look at the installation instruction on node.js website, which
|
||||
covers most popular distributions.</p>
|
||||
<h3>Dependencies</h3>
|
||||
<p>There are some dependencies required. You can see command for Debian and
|
||||
its derivatives (like Ubuntu) below:</p><pre><code class="language-text-x-trilium-auto">sudo apt install libpng16-16 libpng-dev pkg-config autoconf libtool build-essential nasm libx11-dev libxkbfile-dev</code></pre>
|
||||
<h2>Installation</h2>
|
||||
<h3>Download</h3>
|
||||
<p>You can either download source code zip/tar from <a href="https://github.com/TriliumNext/Trilium/releases/latest">https://github.com/TriliumNext/Trilium/releases/latest</a>.</p>
|
||||
<p>For the latest version including betas, clone Git repository <strong>from</strong> <code>main</code> <strong>branch</strong> with:</p><pre><code class="language-text-x-trilium-auto">git clone -b main https://github.com/triliumnext/trilium.git</code></pre>
|
||||
<h2>Installation</h2><pre><code class="language-text-x-trilium-auto">cd trilium
|
||||
|
||||
# download all node dependencies
|
||||
npm install
|
||||
|
||||
# make sure the better-sqlite3 binary is there
|
||||
npm rebuild
|
||||
|
||||
# bundles & minifies frontend JavaScript
|
||||
npm run webpack</code></pre>
|
||||
<h2>Run</h2><pre><code class="language-text-x-trilium-auto">cd trilium
|
||||
|
||||
# using nohup to make sure trilium keeps running after user logs out
|
||||
nohup TRILIUM_ENV=dev node src/www &</code></pre>
|
||||
<p>The application by default starts up on port 8080, so you can open your
|
||||
browser and navigate to <a href="http://localhost:8080">http://localhost:8080</a> to
|
||||
access Trilium (replace "localhost" with your hostname).</p>
|
||||
<h2>TLS</h2>
|
||||
<p>Don't forget to <a href="#root/_help_1G2n2zCStCir">configure TLS</a> which
|
||||
is required for secure usage!</p>
|
||||
@@ -0,0 +1,25 @@
|
||||
<p>Trilium does not support multiple users. In order to have two or more
|
||||
persons with their own set of notes, multiple server instances must be
|
||||
set up. It is also not possible to use multiple <a href="#root/_help_KFhm9yCthQOh">sync</a> servers.</p>
|
||||
<p>To allow multiple server instances on a single physical server:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>For <a class="reference-link" href="#root/_help_Q5jqLlwsnIBb">Packaged version for Linux</a> or
|
||||
<a
|
||||
class="reference-link" href="#root/_help_baqzSqQefEpE">Manually</a>, if starting the server manually just specify a different
|
||||
port and data directory per instance:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_NETWORK_PORT=8080 TRILIUM_DATA_DIR=/path/to/your/data-dir-A /opt/trilium/trilium.sh</code></pre>
|
||||
<p>For a second instance:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_NETWORK_PORT=8081 TRILIUM_DATA_DIR=/path/to/your/data-dir-B /opt/trilium/trilium.sh</code></pre>
|
||||
<p>If using <code>systemd</code>, then set the <a href="https://serverfault.com/questions/413397/how-to-set-environment-variable-in-systemd-service">environment variables in the service configuration</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For <a class="reference-link" href="#root/_help_FNVzcT2FwEjq">Using Docker</a>,
|
||||
simply use two different containers, each with their own port binding and
|
||||
data directory.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>For <a class="reference-link" href="#root/_help_6YVZgQtfkxkS">On NixOS</a>,
|
||||
the only possible way is to use Docker OCI containers or at least one NixOS
|
||||
container with its own service definition.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>For support or additional context, see the related <a href="https://github.com/orgs/TriliumNext/discussions/1642#discussioncomment-12768808">GitHub Discussion</a>.</p>
|
||||
@@ -0,0 +1,15 @@
|
||||
<p>This page describes configuring the Trilium module included in NixOS.</p>
|
||||
<h2>Requirements</h2>
|
||||
<p><a href="https://nixos.org/">NixOS</a> installation.</p>
|
||||
<h2>Configuration</h2>
|
||||
<p>Add this to your <code>configuration.nix</code>:</p><pre><code class="language-text-x-trilium-auto">services.trilium-server.enable = true;
|
||||
|
||||
# default data directory: /var/lib/trilium
|
||||
#services.trilium-server.dataDir = "/var/lib/trilium-sync-server";
|
||||
|
||||
# default bind address: 127.0.0.1, port 8080
|
||||
#services.trilium-server.host = "0.0.0.0";
|
||||
#services.trilium-server.port = 12783;</code></pre>
|
||||
<p>Uncomment any option you would like to change.</p>
|
||||
<p>See the <a href="https://search.nixos.org/options?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=trilium-server">NixOS options list</a> for
|
||||
more options (including nginx reverse proxy configuration).</p>
|
||||
@@ -0,0 +1,167 @@
|
||||
<p>This is essentially Trilium sources + node modules + node.js runtime packaged
|
||||
into one 7z file.</p>
|
||||
<h2>Steps</h2>
|
||||
<ul>
|
||||
<li>SSH into your server</li>
|
||||
<li>use <code>wget</code> (or <code>curl</code>) to download latest <code>TriliumNotes-Server-[VERSION]-linux-x64.tar.xz</code> (copy
|
||||
link from <a href="https://github.com/TriliumNext/Trilium/releases">release page</a>,
|
||||
notice <code>-Server</code> suffix) on your server.</li>
|
||||
<li>unpack the archive, e.g. using <code>tar -xf -d TriliumNotes-Server-[VERSION]-linux-x64.tar.xz</code>
|
||||
</li>
|
||||
<li><code>cd trilium-linux-x64-server</code>
|
||||
</li>
|
||||
<li><code>./trilium.sh</code>
|
||||
</li>
|
||||
<li>you can open the browser and open http://[your-server-hostname]:8080 and
|
||||
you should see Trilium initialization page</li>
|
||||
</ul>
|
||||
<p>The problem with above steps is that once you close the SSH connection,
|
||||
the Trilium process is terminated. To avoid that, you have two options:</p>
|
||||
<ul>
|
||||
<li>Kill it (with e.g. <kbd>Ctrl</kbd> + <kbd>C</kbd>) and run again like this: <code>nohup ./trilium.sh &</code>.
|
||||
(nohup keeps the process running in the background, <code>&</code> runs
|
||||
it in the background)</li>
|
||||
<li>Configure systemd to automatically run Trilium in the background on every
|
||||
boot</li>
|
||||
</ul>
|
||||
<h2>Configure Trilium to auto-run on boot with systemd</h2>
|
||||
<ul>
|
||||
<li>After downloading, extract and move Trilium:</li>
|
||||
</ul><pre><code class="language-text-x-trilium-auto">tar -xvf TriliumNotes-Server-[VERSION]-linux-x64.tar.xz
|
||||
sudo mv trilium-linux-x64-server /opt/trilium</code></pre>
|
||||
<ul>
|
||||
<li>Create the service:</li>
|
||||
</ul><pre><code class="language-text-x-trilium-auto">sudo nano /etc/systemd/system/trilium.service</code></pre>
|
||||
<ul>
|
||||
<li>Paste this into the file (replace the user and group as needed):</li>
|
||||
</ul><pre><code class="language-text-x-trilium-auto">[Unit]
|
||||
Description=Trilium Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=xxx
|
||||
Group=xxx
|
||||
Type=simple
|
||||
ExecStart=/opt/trilium/trilium.sh
|
||||
WorkingDirectory=/opt/trilium/
|
||||
|
||||
TimeoutStopSec=20
|
||||
# KillMode=process leads to error, according to https://www.freedesktop.org/software/systemd/man/systemd.kill.html
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target</code></pre>
|
||||
<ul>
|
||||
<li>Save the file (CTRL-S) and exit (CTRL-X)</li>
|
||||
<li>Enable and launch the service:</li>
|
||||
</ul><pre><code class="language-text-x-trilium-auto">sudo systemctl enable --now -q trilium</code></pre>
|
||||
<ul>
|
||||
<li>You can now open a browser to http://[your-server-hostname]:8080 and you
|
||||
should see the Trilium initialization page.</li>
|
||||
</ul>
|
||||
<h2>Simple Autoupdate for Server</h2>
|
||||
<p>Run as the same User Trilium runs</p>
|
||||
<p>if you run as root please remove 'sudo' from the commands</p>
|
||||
<p>requires "jq" <code>apt install jq</code>
|
||||
</p>
|
||||
<p>It will stop the service above, overwrite everything (i expect no config.ini),
|
||||
and start service It also creates a version file in the Trilium directory
|
||||
so it updates only with a newer Version</p><pre><code class="language-text-x-trilium-auto">#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
REPO="TriliumNext/Trilium"
|
||||
PATTERN="TriliumNotes-Server-.*-linux-x64.tar.xz"
|
||||
DOWNLOAD_DIR="/var/tmp/trilium_download"
|
||||
OUTPUT_DIR="/opt/trilium"
|
||||
SERVICE_NAME="trilium"
|
||||
VERSION_FILE="$OUTPUT_DIR/version.txt"
|
||||
|
||||
# Ensure dependencies are installed
|
||||
command -v curl >/dev/null 2>&1 || { echo "Error: curl is required"; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo "Error: jq is required"; exit 1; }
|
||||
command -v tar >/dev/null 2>&1 || { echo "Error: tar is required"; exit 1; }
|
||||
|
||||
# Create download directory
|
||||
mkdir -p "$DOWNLOAD_DIR" || { echo "Error: Cannot create $DOWNLOAD_DIR"; exit 1; }
|
||||
|
||||
# Get the latest release version
|
||||
LATEST_VERSION=$(curl -sL https://api.github.com/repos/$REPO/releases/latest | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "Error: Could not fetch latest release version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check current installed version (from version.txt or existing tarball)
|
||||
CURRENT_VERSION=""
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
CURRENT_VERSION=$(cat "$VERSION_FILE")
|
||||
elif [ -f "$DOWNLOAD_DIR/TriliumNotes-Server-$LATEST_VERSION-linux-x64.tar.xz" ]; then
|
||||
CURRENT_VERSION="$LATEST_VERSION"
|
||||
fi
|
||||
|
||||
# Compare versions
|
||||
if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then
|
||||
echo "Latest version ($LATEST_VERSION) is already installed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Download the latest release
|
||||
LATEST_URL=$(curl -sL https://api.github.com/repos/$REPO/releases/latest | jq -r ".assets[] | select(.name | test(\"$PATTERN\")) | .browser_download_url")
|
||||
if [ -z "$LATEST_URL" ]; then
|
||||
echo "Error: No asset found matching pattern '$PATTERN'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_NAME=$(basename "$LATEST_URL")
|
||||
FILE_PATH="$DOWNLOAD_DIR/$FILE_NAME"
|
||||
|
||||
# Download if not already present
|
||||
if [ -f "$FILE_PATH" ]; then
|
||||
echo "Latest release $FILE_NAME already downloaded"
|
||||
else
|
||||
curl -LO --output-dir "$DOWNLOAD_DIR" "$LATEST_URL" || { echo "Error: Download failed"; exit 1; }
|
||||
echo "Downloaded $FILE_NAME to $DOWNLOAD_DIR"
|
||||
fi
|
||||
|
||||
# Extract the tarball
|
||||
EXTRACT_DIR="$DOWNLOAD_DIR/extracted"
|
||||
mkdir -p "$EXTRACT_DIR"
|
||||
tar -xJf "$FILE_PATH" -C "$EXTRACT_DIR" || { echo "Error: Extraction failed"; exit 1; }
|
||||
|
||||
# Find the extracted directory (e.g., TriliumNotes-Server-0.97.2-linux-x64)
|
||||
INNER_DIR=$(find "$EXTRACT_DIR" -maxdepth 1 -type d -name "TriliumNotes-Server-*-linux-x64" | head -n 1)
|
||||
if [ -z "$INNER_DIR" ]; then
|
||||
echo "Error: Could not find extracted directory matching TriliumNotes-Server-*-linux-x64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop the trilium-server service
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
echo "Stopping $SERVICE_NAME service..."
|
||||
sudo systemctl stop "$SERVICE_NAME" || { echo "Error: Failed to stop $SERVICE_NAME"; exit 1; }
|
||||
fi
|
||||
|
||||
# Copy contents to /opt/trilium, overwriting existing files
|
||||
echo "Copying contents from $INNER_DIR to $OUTPUT_DIR..."
|
||||
sudo mkdir -p "$OUTPUT_DIR"
|
||||
sudo cp -r "$INNER_DIR"/* "$OUTPUT_DIR"/ || { echo "Error: Copy failed"; exit 1; }
|
||||
echo "$LATEST_VERSION" | sudo tee "$VERSION_FILE" >/dev/null
|
||||
echo "Files copied to $OUTPUT_DIR"
|
||||
|
||||
# Start the trilium-server service
|
||||
echo "Starting $SERVICE_NAME service..."
|
||||
sudo systemctl start "$SERVICE_NAME" || { echo "Error: Failed to start $SERVICE_NAME"; exit 1; }
|
||||
|
||||
# Clean up
|
||||
rm -rf "$EXTRACT_DIR"
|
||||
echo "Cleanup complete. Trilium updated to $LATEST_VERSION."</code></pre>
|
||||
<h2>Common issues</h2>
|
||||
<h3>Outdated glibc</h3><pre><code class="language-text-x-trilium-auto">Error: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /var/www/virtual/.../node_modules/@mlink/scrypt/build/Release/scrypt.node)
|
||||
at Object.Module._extensions..node (module.js:681:18)
|
||||
at Module.load (module.js:565:32)
|
||||
at tryModuleLoad (module.js:505:12)</code></pre>
|
||||
<p>If you get an error like this, you need to either upgrade your glibc (typically
|
||||
by upgrading to up-to-date distribution version) or use some other <a href="#root/_help_U9HWXPbFIRW3">server installation</a> method.</p>
|
||||
<h2>TLS</h2>
|
||||
<p>Don't forget to <a href="#root/_help_1G2n2zCStCir">configure TLS</a>, which
|
||||
is required for secure usage!</p>
|
||||
@@ -0,0 +1,192 @@
|
||||
<p>Official docker images are published on docker hub for <strong>AMD64</strong>, <strong>ARMv7</strong> and <strong>ARM64/v8</strong>:
|
||||
<a
|
||||
href="https://hub.docker.com/r/triliumnext/trilium/">https://hub.docker.com/r/triliumnext/trilium/</a>
|
||||
</p>
|
||||
<h2>Prerequisites</h2>
|
||||
<p>Ensure Docker is installed on your system.</p>
|
||||
<p>If you need help installing Docker, reference the <a href="https://docs.docker.com/engine/install/">Docker Installation Docs</a>
|
||||
</p>
|
||||
<p><strong>Note:</strong> Trilium's Docker container requires root privileges
|
||||
to operate correctly.</p>
|
||||
<aside class="admonition warning">
|
||||
<p>If you're using a SMB/CIFS share or folder as your Trilium data directory,
|
||||
<a
|
||||
href="https://github.com/TriliumNext/Notes/issues/415#issuecomment-2344824400">you'll need</a>to add the mount options of <code>nobrl</code> and <code>noperm</code> when
|
||||
mounting your SMB share.</p>
|
||||
</aside>
|
||||
<h2>Running with Docker Compose</h2>
|
||||
<h3>Grab the latest docker-compose.yml:</h3><pre><code class="language-text-x-trilium-auto">wget https://raw.githubusercontent.com/TriliumNext/Trilium/master/docker-compose.yml</code></pre>
|
||||
<p>Optionally, edit the <code>docker-compose.yml</code> file to configure the
|
||||
container settings prior to starting it. Unless configured otherwise, the
|
||||
data directory will be <code>~/trilium-data</code> and the container will
|
||||
be accessible at port 8080.</p>
|
||||
<h3>Start the container:</h3>
|
||||
<p>Run the following command to start the container in the background:</p><pre><code class="language-text-x-trilium-auto">docker compose up -d</code></pre>
|
||||
<h2>Running without Docker Compose / Further Configuration</h2>
|
||||
<h3>Pulling the Docker Image</h3>
|
||||
<p>To pull the image, use the following command, replacing <code>[VERSION]</code> with
|
||||
the desired version or tag, such as <code>v0.91.6</code> or just <code>latest</code>.
|
||||
(See published tag names at <a href="https://hub.docker.com/r/triliumnext/trilium/tags">https://hub.docker.com/r/triliumnext/trilium/tags</a>.):</p><pre><code class="language-text-x-trilium-auto">docker pull triliumnext/trilium:v0.91.6</code></pre>
|
||||
<p><strong>Warning:</strong> Avoid using the "latest" tag, as it may automatically
|
||||
upgrade your instance to a new minor version, potentially disrupting sync
|
||||
setups or causing other issues.</p>
|
||||
<h3>Preparing the Data Directory</h3>
|
||||
<p>Trilium requires a directory on the host system to store its data. This
|
||||
directory must be mounted into the Docker container with write permissions.</p>
|
||||
<h3>Running the Docker Container</h3>
|
||||
<h4>Local Access Only</h4>
|
||||
<p>Run the container to make it accessible only from the localhost. This
|
||||
setup is suitable for testing or when using a proxy server like Nginx or
|
||||
Apache.</p><pre><code class="language-text-x-trilium-auto">sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]</code></pre>
|
||||
<ol>
|
||||
<li>Verify the container is running using <code>docker ps</code>.</li>
|
||||
<li>Access Trilium via a web browser at <code>127.0.0.1:8080</code>.</li>
|
||||
</ol>
|
||||
<h4>Local Network Access</h4>
|
||||
<p>To make the container accessible only on your local network, first create
|
||||
a new Docker network:</p><pre><code class="language-text-x-trilium-auto">docker network create -d macvlan -o parent=eth0 --subnet 192.168.2.0/24 --gateway 192.168.2.254 --ip-range 192.168.2.252/27 mynet</code></pre>
|
||||
<p>Then, run the container with the network settings:</p><pre><code class="language-text-x-trilium-auto">docker run --net=mynet -d -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:-latest</code></pre>
|
||||
<p>To set a different user ID (UID) and group ID (GID) for the saved data,
|
||||
use the <code>USER_UID</code> and <code>USER_GID</code> environment variables:</p><pre><code class="language-text-x-trilium-auto">docker run --net=mynet -d -p 127.0.0.1:8080:8080 -e "USER_UID=1001" -e "USER_GID=1001" -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:-latest</code></pre>
|
||||
<p>Find the local IP address using <code>docker inspect [container_name]</code> and
|
||||
access the service from devices on the local network.</p><pre><code class="language-text-x-trilium-auto">docker ps
|
||||
docker inspect [container_name]</code></pre>
|
||||
<h4>Global Access</h4>
|
||||
<p>To allow access from any IP address, run the container as follows:</p><pre><code class="language-text-x-trilium-auto">docker run -d -p 0.0.0.0:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]</code></pre>
|
||||
<p>Stop the container with <code>docker stop <CONTAINER ID></code>,
|
||||
where the container ID is obtained from <code>docker ps</code>.</p>
|
||||
<h3>Custom Data Directory</h3>
|
||||
<p>For a custom data directory, use:</p><pre><code class="language-text-x-trilium-auto">-v ~/YourOwnDirectory:/home/node/trilium-data triliumnext/trilium:[VERSION]</code></pre>
|
||||
<p>If you want to run your instance in a non-default way, please use the
|
||||
volume switch as follows: <code>-v ~/YourOwnDirectory:/home/node/trilium-data triliumnext/trilium:<VERSION></code>.
|
||||
It is important to be aware of how Docker works for volumes, with the first
|
||||
path being your own and the second the one to virtually bind to. <a href="https://docs.docker.com/storage/volumes/">https://docs.docker.com/storage/volumes/</a> The
|
||||
path before the colon is the host directory, and the path after the colon
|
||||
is the container's path. More details can be found in the <a href="https://docs.docker.com/storage/volumes/">Docker Volumes Documentation</a>.</p>
|
||||
<h2>Reverse Proxy</h2>
|
||||
<ol>
|
||||
<li><a href="#root/_help_vFhVZ4ZRNxLY">Nginx</a>
|
||||
</li>
|
||||
<li><a href="#root/_help_Eu2zkbyE2QPK">Apache</a>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>Note on --user Directive</h3>
|
||||
<p>The <code>--user</code> directive is unsupported. Instead, use the <code>USER_UID</code> and <code>USER_GID</code> environment
|
||||
variables to set the appropriate user and group IDs.</p>
|
||||
<h3>Note on timezones</h3>
|
||||
<p>If you are having timezone issues and you are not using docker-compose,
|
||||
you may need to add a <code>TZ</code> environment variable with the <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">TZ identifier</a> of
|
||||
your local timezone.</p>
|
||||
<h2>Rootless Docker Image</h2>
|
||||
<aside class="admonition note">
|
||||
<p>Please keep in mind that the data directory is at <code>/home/trilium/trilium-data</code> instead
|
||||
of the typical <code>/home/node/trilium-data</code>. This is because a new
|
||||
user is created and used to run Trilium within the rootless containers.</p>
|
||||
</aside>
|
||||
<p>If you would prefer to run Trilium without having to run the Docker container
|
||||
as <code>root</code>, you can use either of the provided Debian (default)
|
||||
and Alpine-based images with the <code>rootless</code> tag. </p>
|
||||
<p><em><strong>If you're unsure, stick to the “rootful” Docker image referenced above.</strong></em>
|
||||
</p>
|
||||
<p>Below are some commands to pull the rootless images:</p><pre><code class="language-text-x-trilium-auto"># For Debian-based image
|
||||
docker pull triliumnext/trilium:rootless
|
||||
|
||||
# For Alpine-based image
|
||||
docker pull triliumnext/trilium:rootless-alpine</code></pre>
|
||||
<h3>Why Rootless?</h3>
|
||||
<p>Running containers as non-root is a security best practice that reduces
|
||||
the potential impact of container breakouts. If an attacker manages to
|
||||
escape the container, they'll only have the permissions of the non-root
|
||||
user instead of full root access to the host.</p>
|
||||
<h3>How It Works</h3>
|
||||
<p>The rootless Trilium image:</p>
|
||||
<ol>
|
||||
<li>Creates a non-root user (<code>trilium</code>) during build time</li>
|
||||
<li>Configures the application to run as this non-root user</li>
|
||||
<li>Allows runtime customization of the user's UID/GID via Docker's <code>--user</code> flag</li>
|
||||
<li>Does not require a separate Docker <code>entrypoint</code> script</li>
|
||||
</ol>
|
||||
<h3>Usage</h3>
|
||||
<h4><strong>Using docker-compose (Recommended)</strong></h4><pre><code class="language-text-x-trilium-auto"># Run with default UID/GID (1000:1000)
|
||||
docker-compose -f docker-compose.rootless.yml up -d
|
||||
|
||||
# Run with custom UID/GID (e.g., match your host user)
|
||||
TRILIUM_UID=$(id -u) TRILIUM_GID=$(id -g) docker-compose -f docker-compose.rootless.yml up -d
|
||||
|
||||
# Specify a custom data directory
|
||||
TRILIUM_DATA_DIR=/path/to/your/data TRILIUM_UID=$(id -u) TRILIUM_GID=$(id -g) docker-compose -f docker-compose.rootless.yml up -d
|
||||
</code></pre>
|
||||
<h4><strong>Using Docker CLI</strong></h4><pre><code class="language-text-x-trilium-auto"># Build the image
|
||||
docker build -t triliumnext/trilium:rootless -f apps/server/Dockerfile.rootless .
|
||||
|
||||
# Run with default UID/GID (1000:1000)
|
||||
docker run -d --name trilium -p 8080:8080 -v ~/trilium-data:/home/trilium/trilium-data triliumnext/trilium:rootless
|
||||
|
||||
# Run with custom UID/GID
|
||||
docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-data:/home/trilium/trilium-data triliumnext/trilium:rootless
|
||||
</code></pre>
|
||||
<h3>Environment Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_UID</code>: UID to use for the container process (passed
|
||||
to Docker's <code>--user</code> flag)</li>
|
||||
<li><code>TRILIUM_GID</code>: GID to use for the container process (passed
|
||||
to Docker's <code>--user</code> flag)</li>
|
||||
<li><code>TRILIUM_DATA_DIR</code>: Path to the data directory inside the container
|
||||
(default: <code>/home/node/trilium-data</code>)</li>
|
||||
</ul>
|
||||
<p>For a complete list of configuration environment variables (network settings,
|
||||
authentication, sync, etc.), see <a class="reference-link" href="#root/_help_1CEQXvOOO4EK">Configuration (config.ini or environment variables)</a>.</p>
|
||||
<h3>Volume Permissions</h3>
|
||||
<p>If you encounter permission issues with the data volume, ensure that:</p>
|
||||
<ol>
|
||||
<li>The host directory has appropriate permissions for the UID/GID you're
|
||||
using</li>
|
||||
<li>You're setting both <code>TRILIUM_UID</code> and <code>TRILIUM_GID</code> to
|
||||
match the owner of the host directory</li>
|
||||
</ol><pre><code class="language-text-x-trilium-auto"># For example, if your data directory is owned by UID 1001 and GID 1001:
|
||||
TRILIUM_UID=1001 TRILIUM_GID=1001 docker-compose -f docker-compose.rootless.yml up -d
|
||||
</code></pre>
|
||||
<h3>Considerations</h3>
|
||||
<ul>
|
||||
<li>The container starts with a specific UID/GID which can be customized at
|
||||
runtime</li>
|
||||
<li>Unlike the traditional setup, this approach does not use a separate entrypoint
|
||||
script with <code>usermod</code>/<code>groupmod</code> commands</li>
|
||||
<li>The container cannot modify its own UID/GID at runtime, which is a security
|
||||
feature of rootless containers</li>
|
||||
</ul>
|
||||
<h3>Available Rootless Images</h3>
|
||||
<p>Two rootless variants are provided:</p>
|
||||
<ol>
|
||||
<li><strong>Debian-based</strong> (default): Uses the Debian Bullseye Slim
|
||||
base image
|
||||
<ul>
|
||||
<li>Dockerfile: <code>apps/server/Dockerfile.rootless</code>
|
||||
</li>
|
||||
<li>Recommended for most users</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Alpine-based</strong>: Uses the Alpine base image for smaller
|
||||
size
|
||||
<ul>
|
||||
<li>Dockerfile: <code>apps/server/Dockerfile.alpine.rootless</code>
|
||||
</li>
|
||||
<li>Smaller image size, but may have compatibility issues with some systems</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>Building Custom Rootless Images</h3>
|
||||
<p>If you would prefer, you can also customize the UID/GID at build time:</p><pre><code class="language-text-x-trilium-auto"># For Debian-based image with custom UID/GID
|
||||
docker build --build-arg USER=myuser --build-arg UID=1001 --build-arg GID=1001 \
|
||||
-t triliumnext/trilium:rootless-custom -f apps/server/Dockerfile.rootless .
|
||||
|
||||
# For Alpine-based image with custom UID/GID
|
||||
docker build --build-arg USER=myuser --build-arg UID=1001 --build-arg GID=1001 \
|
||||
-t triliumnext/trilium:alpine-rootless-custom -f apps/server/Dockerfile.alpine.rootless .
|
||||
</code></pre>
|
||||
<p>Available build arguments:</p>
|
||||
<ul>
|
||||
<li><code>USER</code>: Username for the non-root user (default: trilium)</li>
|
||||
<li><code>UID</code>: User ID for the non-root user (default: 1000)</li>
|
||||
<li><code>GID</code>: Group ID for the non-root user (default: 1000)</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,33 @@
|
||||
<p>As Trilium can be run in Docker it also can be deployed in Kubernetes.
|
||||
You can either use our Helm chart, a community Helm chart, or roll your
|
||||
own Kubernetes deployment.</p>
|
||||
<p>The recommended way is to use a Helm chart.</p>
|
||||
<h2>Root privileges</h2>
|
||||
<aside class="admonition note">
|
||||
<p>The Trilium container at this time needs to be run with root privileges.
|
||||
It will swap to UID and GID <code>1000:1000</code> to run the <code>node</code> process
|
||||
after execution though, so the main process doesn't run with root privileges.</p>
|
||||
</aside>
|
||||
<p>The Trilium docker container needs to be run with root privileges. The
|
||||
node process inside the container will be started with reduced privileges
|
||||
(uid:gid 1000:1000) after some initialization logic. Please make sure that
|
||||
you don't use a security context (PodSecurityContext) which changes the
|
||||
user ID. To use a different uid:gid for file storage and the application,
|
||||
please use the <code>USER_UID</code> & <code>USER_GID</code> environment
|
||||
variables.</p>
|
||||
<p>The docker image will also fix the permissions of <code>/home/node</code> so
|
||||
you don't have to use an init container.</p>
|
||||
<h2>Helm Charts</h2>
|
||||
<p><a href="https://github.com/TriliumNext/helm-charts">Official Helm chart</a> from
|
||||
TriliumNext Unofficial helm chart by <a href="https://github.com/ohdearaugustin">ohdearaugustin</a>:
|
||||
<a
|
||||
href="https://github.com/ohdearaugustin/charts">https://github.com/ohdearaugustin/charts</a>
|
||||
</p>
|
||||
<h2>Adding a Helm repository</h2>
|
||||
<p>Below is an example of how</p><pre><code class="language-text-x-trilium-auto">helm repo add trilium https://triliumnext.github.io/helm-charts
|
||||
"trilium" has been added to your repositories</code></pre>
|
||||
<h2>How to install a chart</h2>
|
||||
<p>After reviewing the <a href="https://github.com/TriliumNext/helm-charts/blob/main/charts/trilium/values.yaml"><code>values.yaml</code></a> from
|
||||
the Helm chart, modifying as required and then creating your own:</p><pre><code class="language-text-x-trilium-auto">helm install --create-namespace --namespace trilium trilium trilium/trilium -f values.yaml</code></pre>
|
||||
<p>For more information on using Helm, please refer to the Helm documentation,
|
||||
or create a Discussion in the TriliumNext GitHub Organization.</p>
|
||||
79
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.html
generated
vendored
Normal file
79
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.html
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
<p>I've assumed you have created a DNS A record for <code>trilium.yourdomain.com</code> that
|
||||
you want to use for your Trilium server.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Download docker image and create container</p><pre><code class="language-text-x-trilium-auto"> docker pull triliumnext/trilium:[VERSION]
|
||||
docker create --name trilium -t -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Configure Apache proxy and websocket proxy</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Enable apache proxy modules</p><pre><code class="language-text-x-trilium-auto"> a2enmod ssl
|
||||
a2enmod proxy
|
||||
a2enmod proxy_http
|
||||
a2enmod proxy_wstunnel</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Create a new let's encrypt certificate</p><pre><code class="language-text-x-trilium-auto"> sudo certbot certonly -d trilium.mydomain.com</code></pre>
|
||||
<p>Choose standalone (2) and note the location of the created certificates
|
||||
(typically /etc/letsencrypt/live/...)</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Create a new virtual host file for apache (you may want to use <code>apachectl -S</code> to
|
||||
determine the server root location, mine is /etc/apache2)</p><pre><code class="language-text-x-trilium-auto"> sudo nano /etc/apache2/sites-available/trilium.yourdomain.com.conf</code></pre>
|
||||
<p>Paste (and customize) the following text into the configuration file</p><pre><code class="language-text-x-trilium-auto">
|
||||
ServerName http://trilium.yourdomain.com
|
||||
RewriteEngine on
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent]
|
||||
|
||||
|
||||
ServerName https://trilium.yourdomain.com
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Connection} Upgrade [NC]
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteRule /(.*) ws://localhost:8080/$1 [P,L]
|
||||
AllowEncodedSlashes NoDecode
|
||||
ProxyPass / http://localhost:8080/ nocanon
|
||||
ProxyPassReverse / http://localhost:8080/
|
||||
SSLCertificateFile /etc/letsencrypt/live/trilium.yourdomain.com/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/trilium.yourdomain.com/privkey.pem
|
||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Enable the virtual host with <code>sudo a2ensite trilium.yourdomain.com.conf</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Reload apache2 with <code>sudo systemctl reload apache2</code>
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
<p>Create and enable a systemd service to start the docker container on boot</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Create a new empty file called <code>/lib/systemd/system/trilium.service</code> with
|
||||
the contents</p><pre><code class="language-text-x-trilium-auto"> [Unit]
|
||||
Description=Trilium Server
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/docker start -a trilium
|
||||
ExecStop=/usr/bin/docker stop -t 2 trilium
|
||||
|
||||
[Install]
|
||||
WantedBy=local.target</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Install, enable and start service</p><pre><code class="language-text-x-trilium-auto"> sudo systemctl daemon-reload
|
||||
sudo systemctl enable trilium.service
|
||||
sudo systemctl start trilium.service</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
74
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Nginx.html
generated
vendored
Normal file
74
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Nginx.html
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
<p>Configure Nginx proxy and HTTPS. The operating system here is Ubuntu 18.04.</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p>Download Nginx and remove Apache2</p><pre><code class="language-text-x-trilium-auto">sudo apt-get install nginx
|
||||
sudo apt-get remove apache2</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Create configure file</p><pre><code class="language-text-x-trilium-auto">cd /etc/nginx/conf.d
|
||||
vim default.conf</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Fill the file with the context shown below, part of the setting show be
|
||||
changed. Then you can enjoy your web with HTTPS forced and proxy.</p><pre><code class="language-text-x-trilium-auto"># This part configures, where your Trilium server is running
|
||||
upstream trilium {
|
||||
zone trilium 64k;
|
||||
server 127.0.0.1:8080; # change it to a different hostname and port if non-default is used
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
# This part is for proxy and HTTPS configure
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name trilium.example.net; #change trilium.example.net to your domain without HTTPS or HTTP.
|
||||
ssl_certificate /etc/ssl/note/example.crt; #change /etc/ssl/note/example.crt to your path of crt file.
|
||||
ssl_certificate_key /etc/ssl/note/example.net.key; #change /etc/ssl/note/example.net.key to your path of key file.
|
||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
||||
ssl_prefer_server_ciphers on;
|
||||
access_log /var/log/nginx/access.log; #check the path of access.log, if it doesn't fit your file, change it
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://trilium;
|
||||
proxy_read_timeout 90;
|
||||
}
|
||||
}
|
||||
|
||||
# This part is for HTTPS forced
|
||||
server {
|
||||
listen 80;
|
||||
server_name trilium.example.net; # change to your domain
|
||||
return 301 https://$server_name$request_uri;
|
||||
}</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>Alternatively if you want to serve the instance under a different path
|
||||
(useful e.g. if you want to serve multiple instances), update the location
|
||||
block like so:</p>
|
||||
<ul>
|
||||
<li>update the location with your desired path (make sure to not leave a trailing
|
||||
slash "/", if your <code>proxy_pass</code> does not end on a slash as well)</li>
|
||||
<li>add the <code>proxy_cookie_path</code> directive with the same path: this
|
||||
allows you to stay logged in at multiple instances at the same time.</li>
|
||||
</ul><pre><code class="language-text-x-trilium-auto"> location /trilium/instance-one {
|
||||
rewrite /trilium/instance-one/(.*) /$1 break;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://trilium;
|
||||
proxy_cookie_path / /trilium/instance-one
|
||||
proxy_read_timeout 90;
|
||||
}
|
||||
</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
35
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/Authentication.html
generated
vendored
Normal file
35
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/Authentication.html
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
<h2>Disabling authentication</h2>
|
||||
<p>If you are running Trilium on <code>localhost</code> only or if authentication
|
||||
is handled by another component, you can disable Trilium’s authentication
|
||||
by adding the following to <code>config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
|
||||
noAuthentication=true</code></pre>
|
||||
<p>Disabling authentication will bypass even the <a class="reference-link"
|
||||
href="#root/_help_iYzJELZlnPVk">Multi-Factor Authentication</a> since
|
||||
v0.94.1.</p>
|
||||
<h2>Understanding how the session works</h2>
|
||||
<p>Once logged into Trilium, the application will store this information
|
||||
about the login into a cookie on the browser, but also as a session on
|
||||
the server.</p>
|
||||
<p>If “Remember me” is checked, then the login will expire in 21 days. This
|
||||
period can be adjusted by modifying the <code>Session.cookieMaxAge</code> value
|
||||
in <code>config.ini</code>. For example, to have the session expire in one
|
||||
day:</p><pre><code class="language-text-x-trilium-auto">[Session]
|
||||
cookieMaxAge=86400</code></pre>
|
||||
<p>When “Remember me” is unchecked, the behavior is different. At client/browser
|
||||
level the authentication does not have any expiration date, but it will
|
||||
be automatically cleared as soon as the user closes the browser. Nevertheless,
|
||||
the server will also dismiss this authentication in around 24 hours from
|
||||
the <em>last interaction with the application</em>.</p>
|
||||
<h2>Viewing active sessions</h2>
|
||||
<p>The login sessions are now stored in the same <a class="reference-link"
|
||||
href="#root/_help_lvXOQ00dcRlk">Database</a> as the user data. In
|
||||
order to view which sessions are active, open the <a class="reference-link"
|
||||
href="#root/_help_hDJ4mPkZJQ4E">SQL Console</a> and run the following
|
||||
query:</p><pre><code class="language-text-x-trilium-auto">SELECT * FROM sessions</code></pre>
|
||||
<p>Expired sessions are periodically cleaned by the server, generally an
|
||||
hourly interval.</p>
|
||||
<h2>See also</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_iYzJELZlnPVk">Multi-Factor Authentication</a>
|
||||
</li>
|
||||
</ul>
|
||||
103
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/Multi-Factor Authentication.html
generated
vendored
Normal file
103
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/Multi-Factor Authentication.html
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
<p>Multi-factor authentication (MFA) is a security process that requires
|
||||
users to provide two or more verification factors to gain access to a system,
|
||||
application, or account. This adds an extra layer of protection beyond
|
||||
just using a password.</p>
|
||||
<p>By requiring more than one verification method, MFA helps reduce the risk
|
||||
of unauthorized access, even if someone has obtained your password. It’s
|
||||
highly recommended for securing sensitive information stored in your notes.</p>
|
||||
<aside
|
||||
class="admonition warning">
|
||||
<p>OpenID and TOTP cannot be both used at the same time!</p>
|
||||
</aside>
|
||||
<h2>Log in with your Google Account with OpenID!</h2>
|
||||
<p>OpenID is a standardized way to let you log into websites using an account
|
||||
from another service, like Google, to verify your identity.</p>
|
||||
<h2>Why Time-based One Time Passwords?</h2>
|
||||
<p>TOTP (Time-Based One-Time Password) is a security feature that generates
|
||||
a unique, temporary code on your device, like a smartphone, which changes
|
||||
every 30 seconds. You use this code, along with your password, to log into
|
||||
your account, making it much harder for anyone else to access them.</p>
|
||||
<h2>Setup</h2>
|
||||
<p>MFA can only be set up on a server instance.</p>
|
||||
<aside class="admonition note">
|
||||
<p>When Multi-Factor Authentication (MFA) is enabled on a server instance,
|
||||
a new desktop instance may fail to sync with it. As a temporary workaround,
|
||||
you can disable MFA to complete the initial sync, then re-enable MFA afterward.
|
||||
This issue will be addressed in a future release.</p>
|
||||
</aside>
|
||||
<h3>TOTP</h3>
|
||||
<ol>
|
||||
<li>Go to "Menu" -> "Options" -> "MFA"</li>
|
||||
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
|
||||
<li>Choose “Time-Based One-Time Password (TOTP)” under MFA Method</li>
|
||||
<li>Click the "Generate TOTP Secret" button</li>
|
||||
<li>Copy the generated secret to your authentication app/extension</li>
|
||||
<li>Click the "Generate Recovery Codes" button</li>
|
||||
<li>Save the recovery codes. Recovery codes can be used once in place of the
|
||||
TOTP if you loose access to your authenticator. After a rerecovery code
|
||||
is used, it will show the unix timestamp when it was used in the MFA options
|
||||
tab.</li>
|
||||
<li>Re-login will be required after TOTP setup is finished (After you refreshing
|
||||
the page).</li>
|
||||
</ol>
|
||||
<h3>OpenID</h3>
|
||||
<p>In order to setup OpenID, you will need to setup a authentication provider.
|
||||
This requires a bit of extra setup. Follow <a href="https://developers.google.com/identity/openid-connect/openid-connect">these instructions</a> to
|
||||
setup an OpenID service through google. The Redirect URL of Trilium is <code>https://<your-trilium-domain>/callback</code>.</p>
|
||||
<ol>
|
||||
<li>Set the <code>oauthBaseUrl</code>, <code>oauthClientId</code> and <code>oauthClientSecret</code> in
|
||||
the <code>config.ini</code> file (check <a class="reference-link" href="#root/_help_SneMubD5wTR6">Configuration (config.ini or environment variables)</a> for
|
||||
more information).
|
||||
<ol>
|
||||
<li>You can also setup through environment variables:
|
||||
<ul>
|
||||
<li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>
|
||||
</li>
|
||||
<li>Legacy (still supported): <code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code>, <code>TRILIUM_OAUTH_CLIENT_SECRET</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>oauthBaseUrl</code> should be the link of your Trilium instance server,
|
||||
for example, <code>https://<your-trilium-domain></code>.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Restart the server</li>
|
||||
<li>Go to "Menu" -> "Options" -> "MFA"</li>
|
||||
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
|
||||
<li>Choose “OAuth/OpenID” under MFA Method</li>
|
||||
<li>Refresh the page and login through OpenID provider</li>
|
||||
</ol>
|
||||
<aside class="admonition note">
|
||||
<p>The default OAuth issuer is Google. To use other services such as Authentik
|
||||
or Auth0, you can configure the settings via <code>oauthIssuerBaseUrl</code>, <code>oauthIssuerName</code>,
|
||||
and <code>oauthIssuerIcon</code> in the <code>config.ini</code> file. Alternatively,
|
||||
these values can be set using environment variables:</p>
|
||||
<ul>
|
||||
<li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>
|
||||
</li>
|
||||
<li>Legacy (still supported): <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>, <code>TRILIUM_OAUTH_ISSUER_ICON</code>
|
||||
</li>
|
||||
</ul>
|
||||
<p><code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are required
|
||||
for displaying correct issuer information at the Login page.</p>
|
||||
</aside>
|
||||
<h4>Authentik</h4>
|
||||
<p>If you don’t already have a running Authentik instance, please follow
|
||||
<a
|
||||
href="https://docs.goauthentik.io/docs/install-config/install/docker-compose">these instructions</a>to set one up.</p>
|
||||
<ol>
|
||||
<li>In the Authentik admin dashboard, create a new OAuth2 application by following
|
||||
<a
|
||||
href="https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/create-oauth2-provider">these steps</a>. Make sure to set the Redirect URL to: <code>https://<your-trilium-domain>/callback</code>.</li>
|
||||
<li>In your config.ini file, set the relevant OAuth variables:
|
||||
<ol>
|
||||
<li><code>oauthIssuerBaseUrl</code> → Use the <code>OpenID Configuration Issuer</code> URL
|
||||
from your application's overview page.</li>
|
||||
<li><code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> → Set these
|
||||
to customize the name and icon displayed on the login page. If omitted,
|
||||
Google’s name and icon will be shown by default.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Apply the changes by restarting your server.</li>
|
||||
<li>Proceed with the remaining steps starting from Step 3 in the OpenID section.</li>
|
||||
</ol>
|
||||
48
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/TLS Configuration.html
generated
vendored
Normal file
48
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/1_Server Installation/TLS Configuration.html
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
<p>Configuring TLS is essential for <a href="#root/_help_U9HWXPbFIRW3">server installation</a> in
|
||||
Trilium. This guide details the steps to set up TLS within Trilium itself.</p>
|
||||
<p>For a more robust solution, consider using TLS termination with a reverse
|
||||
proxy (recommended, e.g., Nginx). You can follow a <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04">guide like this</a> for
|
||||
such setups.</p>
|
||||
<h2>Obtaining a TLS Certificate</h2>
|
||||
<p>You have two options for obtaining a TLS certificate:</p>
|
||||
<ul>
|
||||
<li><strong>Recommended</strong>: Obtain a TLS certificate signed by a root
|
||||
certificate authority. For personal use, <a href="https://letsencrypt.org">Let's Encrypt</a> is
|
||||
an excellent choice. It is free, automated, and straightforward. Certbot
|
||||
can facilitate automatic TLS setup.</li>
|
||||
<li>Generate a self-signed certificate. This option is not recommended due
|
||||
to the additional complexity of importing the certificate into all machines
|
||||
connecting to the server.</li>
|
||||
</ul>
|
||||
<h2>Modifying <code>config.ini</code></h2>
|
||||
<p>Once you have your certificate, modify the <code>config.ini</code> file
|
||||
in the <a href="#root/_help_dvbMBRXYMM2G">data directory</a> to configure
|
||||
Trilium to use it:</p><pre><code class="language-text-x-trilium-auto">[Network]
|
||||
port=8080
|
||||
# Set to true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
|
||||
https=true
|
||||
# Path to the certificate (run "bash bin/generate-cert.sh" to generate a self-signed certificate).
|
||||
# Relevant only if https=true
|
||||
certPath=/[username]/.acme.sh/[hostname]/fullchain.cer
|
||||
keyPath=/[username]/.acme.sh/[hostname]/example.com.key</code></pre>
|
||||
<p>You can also review the <a href="#root/_help_SneMubD5wTR6">configuration</a> file
|
||||
to provide all <code>config.ini</code> values as environment variables instead.
|
||||
For example, you can configure TLS using environment variables:</p><pre><code class="language-text-x-sh">export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem</code></pre>
|
||||
<p>The above example shows how this is set up in an environment where the
|
||||
certificate was generated using Let's Encrypt's ACME utility. Your paths
|
||||
may differ. For Docker installations, ensure these paths are within a volume
|
||||
or another directory accessible by the Docker container, such as <code>/home/node/trilium-data/[DIR IN DATA DIRECTORY]</code>.</p>
|
||||
<p>After configuring <code>config.ini</code>, restart Trilium and access the
|
||||
hostname using "https".</p>
|
||||
<h2>Self-Signed Certificate</h2>
|
||||
<p>If you opt to use a self-signed certificate for your server instance,
|
||||
note that the desktop instance will not trust it by default.</p>
|
||||
<p>To bypass this, disable certificate validation by setting the following
|
||||
environment variable (for Linux):</p><pre><code class="language-text-x-trilium-auto">export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
trilium</code></pre>
|
||||
<p>Trilium provides scripts to start in this mode, such as <code>trilium-no-cert-check.bat</code> for
|
||||
Windows.</p>
|
||||
<p><strong>Warning</strong>: Disabling TLS certificate validation is insecure.
|
||||
Proceed only if you fully understand the implications.</p>
|
||||
276
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html
generated
vendored
Normal file
276
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html
generated
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
<p>Trilium provides robust encryption capabilities through its Protected
|
||||
Notes system, ensuring your sensitive information remains secure even if
|
||||
your database is compromised.</p>
|
||||
<h2>Overview</h2>
|
||||
<p>Protected notes in Trilium use <strong>AES-128-CBC encryption</strong> with
|
||||
scrypt-based key derivation to protect sensitive content. The encryption
|
||||
is designed to be:</p>
|
||||
<ul>
|
||||
<li><strong>Secure</strong>: Uses industry-standard AES encryption with strong
|
||||
key derivation</li>
|
||||
<li><strong>Selective</strong>: Only notes marked as protected are encrypted</li>
|
||||
<li><strong>Session-based</strong>: Decrypted content remains accessible during
|
||||
a protected session</li>
|
||||
<li><strong>Zero-knowledge</strong>: The server never stores unencrypted protected
|
||||
content</li>
|
||||
</ul>
|
||||
<h2>How Encryption Works</h2>
|
||||
<h3>Encryption Algorithm</h3>
|
||||
<ul>
|
||||
<li><strong>Cipher</strong>: AES-128-CBC (Advanced Encryption Standard in
|
||||
Cipher Block Chaining mode)</li>
|
||||
<li><strong>Key Derivation</strong>: Scrypt with configurable parameters (N=16384,
|
||||
r=8, p=1)</li>
|
||||
<li><strong>Initialization Vector</strong>: 16-byte random IV generated for
|
||||
each encryption operation</li>
|
||||
<li><strong>Integrity Protection</strong>: SHA-1 digest (first 4 bytes) prepended
|
||||
to plaintext for tamper detection</li>
|
||||
</ul>
|
||||
<h3>Key Management</h3>
|
||||
<ol>
|
||||
<li><strong>Master Password</strong>: User-provided password used for key
|
||||
derivation</li>
|
||||
<li><strong>Data Key</strong>: 32-byte random key generated during setup,
|
||||
encrypted with password-derived key</li>
|
||||
<li><strong>Password-Derived Key</strong>: Generated using scrypt from master
|
||||
password and salt</li>
|
||||
<li><strong>Session Key</strong>: Data key loaded into memory during protected
|
||||
session</li>
|
||||
</ol>
|
||||
<h3>Encryption Process</h3><pre><code class="language-text-x-trilium-auto">1. Generate random 16-byte IV
|
||||
2. Compute SHA-1 digest of plaintext (use first 4 bytes)
|
||||
3. Prepend digest to plaintext
|
||||
4. Encrypt (digest + plaintext) using AES-128-CBC
|
||||
5. Prepend IV to encrypted data
|
||||
6. Encode result as Base64</code></pre>
|
||||
<h3>Decryption Process</h3><pre><code class="language-text-x-trilium-auto">1. Decode Base64 ciphertext
|
||||
2. Extract IV (first 16 bytes) and encrypted data
|
||||
3. Decrypt using AES-128-CBC with data key and IV
|
||||
4. Extract digest (first 4 bytes) and plaintext
|
||||
5. Verify integrity by comparing computed vs. stored digest
|
||||
6. Return plaintext if verification succeeds</code></pre>
|
||||
<h2>Setting Up Protected Notes</h2>
|
||||
<h3>Initial Setup</h3>
|
||||
<ol>
|
||||
<li><strong>Set Master Password</strong>: Configure a strong password during
|
||||
initial setup</li>
|
||||
<li><strong>Create Protected Note</strong>: Right-click a note and select
|
||||
"Toggle Protected Status"</li>
|
||||
<li><strong>Enter Protected Session</strong>: Click the shield icon or use
|
||||
Ctrl+Shift+P</li>
|
||||
</ol>
|
||||
<h3>Password Requirements</h3>
|
||||
<ul>
|
||||
<li><strong>Minimum Length</strong>: 8 characters (recommended: 12+ characters)</li>
|
||||
<li><strong>Complexity</strong>: Use a mix of uppercase, lowercase, numbers,
|
||||
and symbols</li>
|
||||
<li><strong>Uniqueness</strong>: Don't reuse passwords from other services</li>
|
||||
<li><strong>Storage</strong>: Consider using a password manager for complex
|
||||
passwords</li>
|
||||
</ul>
|
||||
<h3>Best Practices</h3>
|
||||
<ol>
|
||||
<li><strong>Strong Passwords</strong>: Use passphrases or generated passwords</li>
|
||||
<li><strong>Regular Changes</strong>: Update passwords periodically</li>
|
||||
<li><strong>Secure Storage</strong>: Store password recovery information securely</li>
|
||||
<li><strong>Backup Strategy</strong>: Ensure encrypted backups are properly
|
||||
secured</li>
|
||||
</ol>
|
||||
<h2>Protected Sessions</h2>
|
||||
<h3>Session Management</h3>
|
||||
<ul>
|
||||
<li><strong>Automatic Timeout</strong>: Sessions expire after configurable
|
||||
timeout (default: 10 minutes)</li>
|
||||
<li><strong>Manual Control</strong>: Explicitly enter/exit protected sessions</li>
|
||||
<li><strong>Activity Tracking</strong>: Session timeout resets with each protected
|
||||
note access</li>
|
||||
<li><strong>Multi-client</strong>: Each client maintains its own protected
|
||||
session</li>
|
||||
</ul>
|
||||
<h3>Session Lifecycle</h3>
|
||||
<ol>
|
||||
<li><strong>Enter Session</strong>: User enters master password</li>
|
||||
<li><strong>Key Derivation</strong>: System derives data key from password</li>
|
||||
<li><strong>Session Active</strong>: Protected content accessible in plaintext</li>
|
||||
<li><strong>Timeout/Logout</strong>: Data key removed from memory</li>
|
||||
<li><strong>Protection Restored</strong>: Content returns to encrypted state</li>
|
||||
</ol>
|
||||
<h3>Configuration Options</h3>
|
||||
<p>Access via Options → Protected Session:</p>
|
||||
<ul>
|
||||
<li><strong>Session Timeout</strong>: Duration before automatic logout (seconds)</li>
|
||||
<li><strong>Password Verification</strong>: Enable/disable password strength
|
||||
requirements</li>
|
||||
<li><strong>Recovery Options</strong>: Configure password recovery mechanisms</li>
|
||||
</ul>
|
||||
<h2>Performance Considerations</h2>
|
||||
<h3>Encryption Overhead</h3>
|
||||
<ul>
|
||||
<li><strong>CPU Impact</strong>: Scrypt key derivation is intentionally CPU-intensive</li>
|
||||
<li><strong>Memory Usage</strong>: Minimal additional memory for encrypted
|
||||
content</li>
|
||||
<li><strong>Storage Size</strong>: Encrypted content is slightly larger due
|
||||
to Base64 encoding</li>
|
||||
<li><strong>Network Transfer</strong>: Encrypted notes transfer as Base64
|
||||
strings</li>
|
||||
</ul>
|
||||
<h3>Optimization Tips</h3>
|
||||
<ol>
|
||||
<li><strong>Selective Protection</strong>: Only encrypt truly sensitive notes</li>
|
||||
<li><strong>Session Management</strong>: Keep sessions active during intensive
|
||||
work</li>
|
||||
<li><strong>Hardware Acceleration</strong>: Modern CPUs provide AES acceleration</li>
|
||||
<li><strong>Batch Operations</strong>: Group protected note operations when
|
||||
possible</li>
|
||||
</ol>
|
||||
<h2>Security Considerations</h2>
|
||||
<h3>Threat Model</h3>
|
||||
<p><strong>Protected Against</strong>:</p>
|
||||
<ul>
|
||||
<li>Database theft or unauthorized access</li>
|
||||
<li>Network interception (data at rest)</li>
|
||||
<li>Server-side data breaches</li>
|
||||
<li>Backup file compromise</li>
|
||||
</ul>
|
||||
<p><strong>Not Protected Against</strong>:</p>
|
||||
<ul>
|
||||
<li>Keyloggers or screen capture malware</li>
|
||||
<li>Physical access to unlocked device</li>
|
||||
<li>Memory dumps during active session</li>
|
||||
<li>Social engineering attacks</li>
|
||||
</ul>
|
||||
<h3>Limitations</h3>
|
||||
<ol>
|
||||
<li><strong>Note Titles</strong>: Currently encrypted, may leak structural
|
||||
information</li>
|
||||
<li><strong>Metadata</strong>: Creation dates, modification times remain unencrypted</li>
|
||||
<li><strong>Search Indexing</strong>: Protected notes excluded from full-text
|
||||
search</li>
|
||||
<li><strong>Sync Conflicts</strong>: May be harder to resolve for protected
|
||||
content</li>
|
||||
</ol>
|
||||
<h2>Troubleshooting</h2>
|
||||
<h3>Common Issues</h3>
|
||||
<h4>"Could not decrypt string" Error</h4>
|
||||
<p><strong>Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Incorrect password entered</li>
|
||||
<li>Corrupted encrypted data</li>
|
||||
<li>Database migration issues</li>
|
||||
</ul>
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Verify password spelling and case sensitivity</li>
|
||||
<li>Check for active protected session</li>
|
||||
<li>Restart application and retry</li>
|
||||
<li>Restore from backup if corruption suspected</li>
|
||||
</ol>
|
||||
<h4>Protected Session Won't Start</h4>
|
||||
<p><strong>Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Password verification hash mismatch</li>
|
||||
<li>Missing encryption salt</li>
|
||||
<li>Database schema issues</li>
|
||||
</ul>
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Check error logs for specific error messages</li>
|
||||
<li>Verify database integrity</li>
|
||||
<li>Restore from known good backup</li>
|
||||
<li>Contact support with error details</li>
|
||||
</ol>
|
||||
<h4>Performance Issues</h4>
|
||||
<p><strong>Symptoms</strong>:</p>
|
||||
<ul>
|
||||
<li>Slow password verification</li>
|
||||
<li>Long delays entering protected session</li>
|
||||
<li>High CPU usage during encryption</li>
|
||||
</ul>
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Reduce scrypt parameters (advanced users only)</li>
|
||||
<li>Limit number of protected notes</li>
|
||||
<li>Upgrade hardware (more RAM/faster CPU)</li>
|
||||
<li>Close other resource-intensive applications</li>
|
||||
</ol>
|
||||
<h3>Recovery Procedures</h3>
|
||||
<h4>Password Recovery</h4>
|
||||
<p>If you forget your master password:</p>
|
||||
<ol>
|
||||
<li><strong>No Built-in Recovery</strong>: Trilium cannot recover forgotten
|
||||
passwords</li>
|
||||
<li><strong>Backup Restoration</strong>: Restore from backup with known password</li>
|
||||
<li><strong>Data Export</strong>: Export unprotected content before password
|
||||
change</li>
|
||||
<li><strong>Complete Reset</strong>: Last resort - lose all protected content</li>
|
||||
</ol>
|
||||
<h4>Data Recovery</h4>
|
||||
<p>For corrupted protected notes:</p>
|
||||
<ol>
|
||||
<li><strong>Verify Backup</strong>: Check if backups contain uncorrupted data</li>
|
||||
<li><strong>Export/Import</strong>: Try exporting and re-importing the note</li>
|
||||
<li><strong>Database Repair</strong>: Use database repair tools if available</li>
|
||||
<li><strong>Professional Help</strong>: Contact data recovery services for
|
||||
critical data</li>
|
||||
</ol>
|
||||
<h2>Advanced Configuration</h2>
|
||||
<h3>Custom Encryption Parameters</h3>
|
||||
<p><strong>Warning</strong>: Modifying encryption parameters requires advanced
|
||||
knowledge and may break compatibility.</p>
|
||||
<p>For expert users, encryption parameters can be modified in the source
|
||||
code:</p><pre><code class="language-application-typescript">// In my_scrypt.ts
|
||||
const scryptParams = {
|
||||
N: 16384, // CPU/memory cost parameter
|
||||
r: 8, // Block size parameter
|
||||
p: 1 // Parallelization parameter
|
||||
};</code></pre>
|
||||
<h3>Integration with External Tools</h3>
|
||||
<p>Protected notes can be accessed programmatically:</p><pre><code class="language-application-javascript-env-backend">// Backend script example
|
||||
const protectedNote = api.getNote('noteId');
|
||||
if (protectedNote.isProtected) {
|
||||
// Content will be encrypted unless in protected session
|
||||
const content = protectedNote.getContent();
|
||||
}</code></pre>
|
||||
<h2>Compliance and Auditing</h2>
|
||||
<h3>Encryption Standards</h3>
|
||||
<ul>
|
||||
<li><strong>Algorithm</strong>: AES-128-CBC (FIPS 140-2 approved)</li>
|
||||
<li><strong>Key Derivation</strong>: Scrypt (RFC 7914)</li>
|
||||
<li><strong>Random Generation</strong>: Node.js crypto.randomBytes() (OS entropy)</li>
|
||||
</ul>
|
||||
<h3>Audit Trail</h3>
|
||||
<ul>
|
||||
<li>Protected session entry/exit events logged</li>
|
||||
<li>Encryption/decryption operations tracked</li>
|
||||
<li>Password verification attempts recorded</li>
|
||||
<li>Key derivation operations monitored</li>
|
||||
</ul>
|
||||
<h3>Compliance Considerations</h3>
|
||||
<ul>
|
||||
<li><strong>GDPR</strong>: Encryption provides data protection safeguards</li>
|
||||
<li><strong>HIPAA</strong>: AES encryption meets security requirements</li>
|
||||
<li><strong>SOX</strong>: Audit trails support compliance requirements</li>
|
||||
<li><strong>PCI DSS</strong>: Strong encryption protects sensitive data</li>
|
||||
</ul>
|
||||
<h2>Migration and Backup</h2>
|
||||
<h3>Backup Strategies</h3>
|
||||
<ol>
|
||||
<li><strong>Encrypted Backups</strong>: Regular backups preserve encrypted
|
||||
state</li>
|
||||
<li><strong>Unencrypted Exports</strong>: Export protected content during
|
||||
session</li>
|
||||
<li><strong>Key Management</strong>: Securely store password recovery information</li>
|
||||
<li><strong>Testing</strong>: Regularly test backup restoration procedures</li>
|
||||
</ol>
|
||||
<h3>Migration Procedures</h3>
|
||||
<p>When moving to new installation:</p>
|
||||
<ol>
|
||||
<li><strong>Export Data</strong>: Export all notes including protected content</li>
|
||||
<li><strong>Backup Database</strong>: Create complete database backup</li>
|
||||
<li><strong>Transfer Files</strong>: Move exported files to new installation</li>
|
||||
<li><strong>Import Data</strong>: Import using same master password</li>
|
||||
<li><strong>Verify</strong>: Confirm all protected content accessible</li>
|
||||
</ol>
|
||||
<p>Remember: The security of protected notes ultimately depends on choosing
|
||||
a strong master password and following security best practices for your
|
||||
overall system.</p>
|
||||
511
docs/Developer Guide/!!!meta.json
vendored
511
docs/Developer Guide/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.97.2",
|
||||
"appVersion": "0.98.0",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
@@ -19,6 +19,503 @@
|
||||
"attachments": [],
|
||||
"dirFileName": "Developer Guide",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "YNX9NN659vF2",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2"
|
||||
],
|
||||
"title": "Architecture",
|
||||
"notePosition": 80,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"attachments": [],
|
||||
"dirFileName": "Architecture",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "yfKVWBgPMQj4",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"yfKVWBgPMQj4"
|
||||
],
|
||||
"title": "API-Architecture",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "1HpEf2wiYVmV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "9GSipWxTZ81I",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "API-Architecture.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "9GSipWxTZ81I",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"9GSipWxTZ81I"
|
||||
],
|
||||
"title": "Entity-System",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "1HpEf2wiYVmV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Entity-System.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "QR0jtNmTvYqx",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"QR0jtNmTvYqx"
|
||||
],
|
||||
"title": "Monorepo-Structure",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Monorepo-Structure.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "KQT5Xda5Czcl",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"KQT5Xda5Czcl"
|
||||
],
|
||||
"title": "README",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "1HpEf2wiYVmV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "9GSipWxTZ81I",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "NZV7rSwYe0dV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "yfKVWBgPMQj4",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "QR0jtNmTvYqx",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "README.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "1HpEf2wiYVmV",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"1HpEf2wiYVmV"
|
||||
],
|
||||
"title": "Three-Layer-Cache-System",
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "9GSipWxTZ81I",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Three-Layer-Cache-System.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "NZV7rSwYe0dV",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"YNX9NN659vF2",
|
||||
"NZV7rSwYe0dV"
|
||||
],
|
||||
"title": "Widget-Based-UI-Architecture",
|
||||
"notePosition": 60,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Widget-Based-UI-Architecture.md",
|
||||
"attachments": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "TfoaMQkD75JI",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI"
|
||||
],
|
||||
"title": "API Documentation",
|
||||
"notePosition": 90,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"attachments": [],
|
||||
"dirFileName": "API Documentation",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "tvrq35NqaWce",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"tvrq35NqaWce"
|
||||
],
|
||||
"title": "API Client Libraries",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oB5PbEHAzG9t",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "eDWmn0B42ALV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "RqCs8jiZGB1g",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "API Client Libraries.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "oB5PbEHAzG9t",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"oB5PbEHAzG9t"
|
||||
],
|
||||
"title": "ETAPI Complete Guide",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "ETAPI Complete Guide.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "4sxya4t9P8AA",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"4sxya4t9P8AA"
|
||||
],
|
||||
"title": "Internal API Reference",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oB5PbEHAzG9t",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "RqCs8jiZGB1g",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "eDWmn0B42ALV",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Internal API Reference.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "RqCs8jiZGB1g",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"RqCs8jiZGB1g"
|
||||
],
|
||||
"title": "Script API Cookbook",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Script API Cookbook.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "eDWmn0B42ALV",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"TfoaMQkD75JI",
|
||||
"eDWmn0B42ALV"
|
||||
],
|
||||
"title": "WebSocket API",
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "4sxya4t9P8AA",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "oB5PbEHAzG9t",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"name": "internalLink",
|
||||
"value": "tvrq35NqaWce",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "markdown",
|
||||
"dataFileName": "WebSocket API.md",
|
||||
"attachments": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "bPMgREWrBYg4",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4"
|
||||
],
|
||||
"title": "Plugin Development",
|
||||
"notePosition": 100,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"attachments": [],
|
||||
"dirFileName": "Plugin Development",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "bZEM5pnOzTVt",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"bZEM5pnOzTVt"
|
||||
],
|
||||
"title": "Backend Script Development",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Backend Script Development.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "fI5sL6zd3n9K",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"fI5sL6zd3n9K"
|
||||
],
|
||||
"title": "Custom Note Type Development",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Custom Note Type Development.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "gRXGO3vGuey9",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"gRXGO3vGuey9"
|
||||
],
|
||||
"title": "Custom Widget Development Guide",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Custom Widget Development Guid.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "3ZeRzd0e7Btc",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"3ZeRzd0e7Btc"
|
||||
],
|
||||
"title": "Frontend Script Development",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Frontend Script Development.md",
|
||||
"attachments": []
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "dZnOJuAVLKxt",
|
||||
"notePath": [
|
||||
"jdjRLhLV3TtI",
|
||||
"bPMgREWrBYg4",
|
||||
"dZnOJuAVLKxt"
|
||||
],
|
||||
"title": "Theme Development Guide",
|
||||
"notePosition": 50,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/markdown",
|
||||
"attributes": [],
|
||||
"format": "markdown",
|
||||
"dataFileName": "Theme Development Guide.md",
|
||||
"attachments": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "T2W7WCZrYZBU",
|
||||
@@ -27,7 +524,7 @@
|
||||
"T2W7WCZrYZBU"
|
||||
],
|
||||
"title": "Environment Setup",
|
||||
"notePosition": 50,
|
||||
"notePosition": 130,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -53,7 +550,7 @@
|
||||
"cxfTSHIUQtt2"
|
||||
],
|
||||
"title": "Project Structure",
|
||||
"notePosition": 190,
|
||||
"notePosition": 270,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -139,7 +636,7 @@
|
||||
"YjerxU7Aii8X"
|
||||
],
|
||||
"title": "Troubleshooting",
|
||||
"notePosition": 200,
|
||||
"notePosition": 280,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -186,7 +683,7 @@
|
||||
"wbVIolLKDhe2"
|
||||
],
|
||||
"title": "Development and architecture",
|
||||
"notePosition": 220,
|
||||
"notePosition": 300,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -1728,7 +2225,7 @@
|
||||
"VHhyVRYK43gI"
|
||||
],
|
||||
"title": "Building and deployment",
|
||||
"notePosition": 230,
|
||||
"notePosition": 310,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
@@ -1767,7 +2264,7 @@
|
||||
"ibAPHul7Efvr"
|
||||
],
|
||||
"title": "Old documentation",
|
||||
"notePosition": 260,
|
||||
"notePosition": 340,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
|
||||
2277
docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md
vendored
Normal file
2277
docs/Developer Guide/Developer Guide/API Documentation/API Client Libraries.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1798
docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md
vendored
Normal file
1798
docs/Developer Guide/Developer Guide/API Documentation/ETAPI Complete Guide.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1926
docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md
vendored
Normal file
1926
docs/Developer Guide/Developer Guide/API Documentation/Internal API Reference.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1845
docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md
vendored
Normal file
1845
docs/Developer Guide/Developer Guide/API Documentation/Script API Cookbook.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1795
docs/Developer Guide/Developer Guide/API Documentation/WebSocket API.md
vendored
Normal file
1795
docs/Developer Guide/Developer Guide/API Documentation/WebSocket API.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
900
docs/Developer Guide/Developer Guide/Architecture/API-Architecture.md
vendored
Normal file
900
docs/Developer Guide/Developer Guide/Architecture/API-Architecture.md
vendored
Normal file
@@ -0,0 +1,900 @@
|
||||
# API-Architecture
|
||||
## API Architecture
|
||||
|
||||
Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization. This document details each API layer's design, usage, and best practices.
|
||||
|
||||
## API Layers Overview
|
||||
|
||||
```
|
||||
graph TB
|
||||
subgraph "Client Applications"
|
||||
WebApp[Web Application]
|
||||
Desktop[Desktop App]
|
||||
Mobile[Mobile App]
|
||||
External[External Apps]
|
||||
Scripts[User Scripts]
|
||||
end
|
||||
|
||||
subgraph "API Layers"
|
||||
Internal[Internal API<br/>REST + WebSocket]
|
||||
ETAPI[ETAPI<br/>External API]
|
||||
WS[WebSocket<br/>Real-time Sync]
|
||||
end
|
||||
|
||||
subgraph "Backend Services"
|
||||
Routes[Route Handlers]
|
||||
Services[Business Logic]
|
||||
Becca[Becca Cache]
|
||||
DB[(Database)]
|
||||
end
|
||||
|
||||
WebApp --> Internal
|
||||
Desktop --> Internal
|
||||
Mobile --> Internal
|
||||
External --> ETAPI
|
||||
Scripts --> ETAPI
|
||||
|
||||
Internal --> Routes
|
||||
ETAPI --> Routes
|
||||
WS --> Services
|
||||
|
||||
Routes --> Services
|
||||
Services --> Becca
|
||||
Becca --> DB
|
||||
|
||||
style Internal fill:#e3f2fd
|
||||
style ETAPI fill:#fff3e0
|
||||
style WS fill:#f3e5f5
|
||||
```
|
||||
|
||||
## Internal API
|
||||
|
||||
**Location**: `/apps/server/src/routes/api/`
|
||||
|
||||
The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// Route structure
|
||||
/api/
|
||||
├── notes.ts // Note operations
|
||||
├── branches.ts // Branch management
|
||||
├── attributes.ts // Attribute operations
|
||||
├── tree.ts // Tree structure
|
||||
├── search.ts // Search functionality
|
||||
├── sync.ts // Synchronization
|
||||
├── options.ts // Configuration
|
||||
└── special.ts // Special operations
|
||||
```
|
||||
|
||||
### Request/Response Pattern
|
||||
|
||||
```typescript
|
||||
// Typical API endpoint structure
|
||||
router.get('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).json({
|
||||
error: 'Note not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
|
||||
router.put('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||
|
||||
note.title = req.body.title;
|
||||
note.content = req.body.content;
|
||||
note.save();
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
#### Note Operations
|
||||
|
||||
```typescript
|
||||
// Get note with content
|
||||
GET /api/notes/:noteId
|
||||
Response: {
|
||||
noteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content: string,
|
||||
dateCreated: string,
|
||||
dateModified: string
|
||||
}
|
||||
|
||||
// Update note
|
||||
PUT /api/notes/:noteId
|
||||
Body: {
|
||||
title?: string,
|
||||
content?: string,
|
||||
type?: string,
|
||||
mime?: string
|
||||
}
|
||||
|
||||
// Create note
|
||||
POST /api/notes/:parentNoteId/children
|
||||
Body: {
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /api/notes/:noteId
|
||||
```
|
||||
|
||||
#### Tree Operations
|
||||
|
||||
```typescript
|
||||
// Get tree structure
|
||||
GET /api/tree
|
||||
Query: {
|
||||
subTreeNoteId?: string,
|
||||
includeAttributes?: boolean
|
||||
}
|
||||
Response: {
|
||||
notes: FNoteRow[],
|
||||
branches: FBranchRow[],
|
||||
attributes: FAttributeRow[]
|
||||
}
|
||||
|
||||
// Move branch
|
||||
PUT /api/branches/:branchId/move
|
||||
Body: {
|
||||
parentNoteId: string,
|
||||
position: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Search Operations
|
||||
|
||||
```typescript
|
||||
// Execute search
|
||||
GET /api/search
|
||||
Query: {
|
||||
query: string,
|
||||
fastSearch?: boolean,
|
||||
includeArchivedNotes?: boolean,
|
||||
ancestorNoteId?: string
|
||||
}
|
||||
Response: {
|
||||
results: Array<{
|
||||
noteId: string,
|
||||
title: string,
|
||||
path: string,
|
||||
score: number
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication & Security
|
||||
|
||||
```typescript
|
||||
// CSRF protection
|
||||
app.use(csrfMiddleware);
|
||||
|
||||
// Session authentication
|
||||
router.use((req, res, next) => {
|
||||
if (!req.session.loggedIn) {
|
||||
return res.status(401).json({
|
||||
error: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Protected note access
|
||||
router.get('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
return res.status(403).json({
|
||||
error: 'Protected session required'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
```
|
||||
|
||||
## ETAPI (External API)
|
||||
|
||||
**Location**: `/apps/server/src/etapi/`
|
||||
|
||||
ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// ETAPI structure
|
||||
/etapi/
|
||||
├── etapi.openapi.yaml // OpenAPI specification
|
||||
├── auth.ts // Authentication
|
||||
├── notes.ts // Note endpoints
|
||||
├── branches.ts // Branch endpoints
|
||||
├── attributes.ts // Attribute endpoints
|
||||
├── attachments.ts // Attachment endpoints
|
||||
└── special_notes.ts // Special note operations
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
ETAPI uses token-based authentication:
|
||||
|
||||
```typescript
|
||||
// Creating ETAPI token
|
||||
POST /etapi/auth/login
|
||||
Body: {
|
||||
username: string,
|
||||
password: string
|
||||
}
|
||||
Response: {
|
||||
authToken: string
|
||||
}
|
||||
|
||||
// Using token in requests
|
||||
GET /etapi/notes/:noteId
|
||||
Headers: {
|
||||
Authorization: "authToken"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
#### Note CRUD Operations
|
||||
|
||||
```typescript
|
||||
// Create note
|
||||
POST /etapi/notes
|
||||
Body: {
|
||||
noteId?: string,
|
||||
parentNoteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Get note
|
||||
GET /etapi/notes/:noteId
|
||||
Response: {
|
||||
noteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
mime: string,
|
||||
isProtected: boolean,
|
||||
attributes: Array<{
|
||||
attributeId: string,
|
||||
type: string,
|
||||
name: string,
|
||||
value: string
|
||||
}>,
|
||||
parentNoteIds: string[],
|
||||
childNoteIds: string[],
|
||||
dateCreated: string,
|
||||
dateModified: string
|
||||
}
|
||||
|
||||
// Update note content
|
||||
PUT /etapi/notes/:noteId/content
|
||||
Body: string | Buffer
|
||||
Headers: {
|
||||
"Content-Type": mime-type
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /etapi/notes/:noteId
|
||||
```
|
||||
|
||||
#### Attribute Management
|
||||
|
||||
```typescript
|
||||
// Create attribute
|
||||
POST /etapi/attributes
|
||||
Body: {
|
||||
noteId: string,
|
||||
type: 'label' | 'relation',
|
||||
name: string,
|
||||
value: string,
|
||||
isInheritable?: boolean
|
||||
}
|
||||
|
||||
// Update attribute
|
||||
PATCH /etapi/attributes/:attributeId
|
||||
Body: {
|
||||
value?: string,
|
||||
isInheritable?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
#### Search
|
||||
|
||||
```typescript
|
||||
// Search notes
|
||||
GET /etapi/notes/search
|
||||
Query: {
|
||||
search: string,
|
||||
limit?: number,
|
||||
orderBy?: string,
|
||||
orderDirection?: 'asc' | 'desc'
|
||||
}
|
||||
Response: {
|
||||
results: Array<{
|
||||
noteId: string,
|
||||
title: string,
|
||||
// Other note properties
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
### Client Libraries
|
||||
|
||||
```javascript
|
||||
// JavaScript client example
|
||||
class EtapiClient {
|
||||
constructor(serverUrl, authToken) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
async getNote(noteId) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes/${noteId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': this.authToken
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createNote(parentNoteId, title, content) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': this.authToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parentNoteId,
|
||||
title,
|
||||
type: 'text',
|
||||
content
|
||||
})
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python Client Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
class TriliumETAPI:
|
||||
def __init__(self, server_url, auth_token):
|
||||
self.server_url = server_url
|
||||
self.auth_token = auth_token
|
||||
self.headers = {'Authorization': auth_token}
|
||||
|
||||
def get_note(self, note_id):
|
||||
response = requests.get(
|
||||
f"{self.server_url}/etapi/notes/{note_id}",
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def create_note(self, parent_note_id, title, content=""):
|
||||
response = requests.post(
|
||||
f"{self.server_url}/etapi/notes",
|
||||
headers=self.headers,
|
||||
json={
|
||||
'parentNoteId': parent_note_id,
|
||||
'title': title,
|
||||
'type': 'text',
|
||||
'content': content
|
||||
}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def search_notes(self, query):
|
||||
response = requests.get(
|
||||
f"{self.server_url}/etapi/notes/search",
|
||||
headers=self.headers,
|
||||
params={'search': query}
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## WebSocket Real-time Synchronization
|
||||
|
||||
**Location**: `/apps/server/src/services/ws.ts`
|
||||
|
||||
WebSocket connections provide real-time updates and synchronization between clients.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// WebSocket message types
|
||||
interface WSMessage {
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// Common message types
|
||||
type MessageType =
|
||||
| 'entity-changes' // Entity updates
|
||||
| 'sync' // Sync events
|
||||
| 'note-content-change' // Content updates
|
||||
| 'refresh-tree' // Tree structure changes
|
||||
| 'options-changed' // Configuration updates
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
// Client connection
|
||||
const ws = new WebSocket('wss://server/ws');
|
||||
|
||||
ws.on('open', () => {
|
||||
// Authenticate
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: sessionToken
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
handleWSMessage(message);
|
||||
});
|
||||
|
||||
// Server-side handling
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const session = parseSession(req);
|
||||
|
||||
if (!session.authenticated) {
|
||||
ws.close(1008, 'Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
clients.add(ws);
|
||||
|
||||
ws.on('message', (message) => {
|
||||
handleClientMessage(ws, message);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Message Broadcasting
|
||||
|
||||
```typescript
|
||||
// Broadcast entity changes
|
||||
function broadcastEntityChanges(changes: EntityChange[]) {
|
||||
const message = {
|
||||
type: 'entity-changes',
|
||||
data: changes
|
||||
};
|
||||
|
||||
for (const client of clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Targeted messages
|
||||
function sendToClient(clientId: string, message: WSMessage) {
|
||||
const client = clients.get(clientId);
|
||||
if (client?.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Sync Protocol
|
||||
|
||||
```typescript
|
||||
// Entity change notification
|
||||
{
|
||||
type: 'entity-changes',
|
||||
data: [
|
||||
{
|
||||
entityName: 'notes',
|
||||
entityId: 'noteId123',
|
||||
action: 'update',
|
||||
entity: { /* note data */ }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Sync pull request
|
||||
{
|
||||
type: 'sync-pull',
|
||||
data: {
|
||||
lastSyncId: 12345
|
||||
}
|
||||
}
|
||||
|
||||
// Sync push
|
||||
{
|
||||
type: 'sync-push',
|
||||
data: {
|
||||
entities: [ /* changed entities */ ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client-side Handling
|
||||
|
||||
```typescript
|
||||
// Froca WebSocket integration
|
||||
class WSClient {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.reconnectTimeout = null;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.getWSUrl());
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
// Reconnect with exponential backoff
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(message: WSMessage) {
|
||||
switch (message.type) {
|
||||
case 'entity-changes':
|
||||
this.handleEntityChanges(message.data);
|
||||
break;
|
||||
case 'refresh-tree':
|
||||
froca.loadInitialTree();
|
||||
break;
|
||||
case 'note-content-change':
|
||||
this.handleContentChange(message.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleEntityChanges(changes: EntityChange[]) {
|
||||
for (const change of changes) {
|
||||
if (change.entityName === 'notes') {
|
||||
froca.reloadNotes([change.entityId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
```typescript
|
||||
// 1. Session-based (Internal API)
|
||||
app.use(session({
|
||||
secret: config.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
// 2. Token-based (ETAPI)
|
||||
router.use('/etapi', (req, res, next) => {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
const etapiToken = becca.getEtapiToken(token);
|
||||
if (!etapiToken || etapiToken.isExpired()) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid or expired token'
|
||||
});
|
||||
}
|
||||
|
||||
req.etapiToken = etapiToken;
|
||||
next();
|
||||
});
|
||||
|
||||
// 3. WebSocket authentication
|
||||
ws.on('connection', (socket) => {
|
||||
socket.on('auth', (token) => {
|
||||
if (!validateToken(token)) {
|
||||
socket.close(1008, 'Invalid token');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// Global rate limit
|
||||
const globalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000 // limit each IP to 1000 requests per windowMs
|
||||
});
|
||||
|
||||
// Strict limit for authentication
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
message: 'Too many authentication attempts'
|
||||
});
|
||||
|
||||
app.use('/api', globalLimiter);
|
||||
app.use('/api/auth', authLimiter);
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```typescript
|
||||
import { body, validationResult } from 'express-validator';
|
||||
|
||||
router.post('/api/notes',
|
||||
body('title').isString().isLength({ min: 1, max: 1000 }),
|
||||
body('type').isIn(['text', 'code', 'file', 'image']),
|
||||
body('content').optional().isString(),
|
||||
(req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// Process valid input
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Caching Strategies
|
||||
|
||||
```typescript
|
||||
// Response caching
|
||||
const cache = new Map();
|
||||
|
||||
router.get('/api/notes/:noteId', (req, res) => {
|
||||
const cacheKey = `note:${req.params.noteId}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return res.json(cached.data);
|
||||
}
|
||||
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
const data = note.getPojo();
|
||||
|
||||
cache.set(cacheKey, {
|
||||
data,
|
||||
expires: Date.now() + 60000 // 1 minute
|
||||
});
|
||||
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Batch API endpoint
|
||||
router.post('/api/batch', async (req, res) => {
|
||||
const operations = req.body.operations;
|
||||
const results = [];
|
||||
|
||||
await sql.transactional(async () => {
|
||||
for (const op of operations) {
|
||||
const result = await executeOperation(op);
|
||||
results.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ results });
|
||||
});
|
||||
|
||||
// Client batch usage
|
||||
const batch = [
|
||||
{ method: 'PUT', path: '/notes/1', body: { title: 'Note 1' }},
|
||||
{ method: 'PUT', path: '/notes/2', body: { title: 'Note 2' }},
|
||||
{ method: 'POST', path: '/notes/3/attributes', body: { type: 'label', name: 'todo' }}
|
||||
];
|
||||
|
||||
await api.post('/batch', { operations: batch });
|
||||
```
|
||||
|
||||
### Streaming Responses
|
||||
|
||||
```typescript
|
||||
// Stream large data
|
||||
router.get('/api/export', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
'Transfer-Encoding': 'chunked'
|
||||
});
|
||||
|
||||
const noteStream = createNoteExportStream();
|
||||
|
||||
noteStream.on('data', (note) => {
|
||||
res.write(JSON.stringify(note) + '\n');
|
||||
});
|
||||
|
||||
noteStream.on('end', () => {
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Standard Error Responses
|
||||
|
||||
```typescript
|
||||
// Error response format
|
||||
interface ErrorResponse {
|
||||
error: string;
|
||||
code?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error('API Error:', err);
|
||||
|
||||
if (err instanceof NotFoundError) {
|
||||
return res.status(404).json({
|
||||
error: err.message,
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
if (err instanceof ValidationError) {
|
||||
return res.status(400).json({
|
||||
error: err.message,
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: err.details
|
||||
});
|
||||
}
|
||||
|
||||
// Generic error
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
code: 'INTERNAL_ERROR'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### OpenAPI/Swagger
|
||||
|
||||
```yaml
|
||||
# etapi.openapi.yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Trilium ETAPI
|
||||
version: 1.0.0
|
||||
description: External API for Trilium Notes
|
||||
|
||||
paths:
|
||||
/etapi/notes/{noteId}:
|
||||
get:
|
||||
summary: Get note by ID
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Note found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Note'
|
||||
404:
|
||||
description: Note not found
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Note:
|
||||
type: object
|
||||
properties:
|
||||
noteId:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [text, code, file, image]
|
||||
```
|
||||
|
||||
### API Testing
|
||||
|
||||
```typescript
|
||||
// API test example
|
||||
describe('Notes API', () => {
|
||||
it('should create a note', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/notes/root/children')
|
||||
.send({
|
||||
title: 'Test Note',
|
||||
type: 'text',
|
||||
content: 'Test content'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('noteId');
|
||||
expect(response.body.title).toBe('Test Note');
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/notes/invalid')
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### API Design
|
||||
|
||||
1. **RESTful conventions**: Use appropriate HTTP methods and status codes
|
||||
2. **Consistent naming**: Use camelCase for JSON properties
|
||||
3. **Versioning**: Version the API to maintain compatibility
|
||||
4. **Documentation**: Keep OpenAPI spec up to date
|
||||
|
||||
### Security
|
||||
|
||||
1. **Authentication**: Always verify user identity
|
||||
2. **Authorization**: Check permissions for each operation
|
||||
3. **Validation**: Validate all input data
|
||||
4. **Rate limiting**: Prevent abuse with appropriate limits
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Pagination**: Limit response sizes with pagination
|
||||
2. **Caching**: Cache frequently accessed data
|
||||
3. **Batch operations**: Support bulk operations
|
||||
4. **Async processing**: Use queues for long-running tasks
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
|
||||
* [Entity System](Entity-System.md) - Data model
|
||||
* [ETAPI Reference](#root/YCSbHgSqH9PA) - OpenAPI specification
|
||||
614
docs/Developer Guide/Developer Guide/Architecture/Entity-System.md
vendored
Normal file
614
docs/Developer Guide/Developer Guide/Architecture/Entity-System.md
vendored
Normal file
@@ -0,0 +1,614 @@
|
||||
# Entity-System
|
||||
## Entity System Architecture
|
||||
|
||||
The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns.
|
||||
|
||||
## Core Entities Overview
|
||||
|
||||
```
|
||||
erDiagram
|
||||
Note ||--o{ Branch : "parent-child"
|
||||
Note ||--o{ Attribute : "has"
|
||||
Note ||--o{ Revision : "history"
|
||||
Note ||--o{ Attachment : "contains"
|
||||
Attachment ||--|| Blob : "stores in"
|
||||
Revision ||--|| Blob : "stores in"
|
||||
Note }o--o{ Note : "relates via Attribute"
|
||||
|
||||
Note {
|
||||
string noteId PK
|
||||
string title
|
||||
string type
|
||||
string content
|
||||
boolean isProtected
|
||||
string dateCreated
|
||||
string dateModified
|
||||
}
|
||||
|
||||
Branch {
|
||||
string branchId PK
|
||||
string noteId FK
|
||||
string parentNoteId FK
|
||||
integer notePosition
|
||||
string prefix
|
||||
boolean isExpanded
|
||||
}
|
||||
|
||||
Attribute {
|
||||
string attributeId PK
|
||||
string noteId FK
|
||||
string type "label or relation"
|
||||
string name
|
||||
string value
|
||||
integer position
|
||||
boolean isInheritable
|
||||
}
|
||||
|
||||
Revision {
|
||||
string revisionId PK
|
||||
string noteId FK
|
||||
string title
|
||||
string type
|
||||
boolean isProtected
|
||||
string dateCreated
|
||||
}
|
||||
|
||||
Attachment {
|
||||
string attachmentId PK
|
||||
string ownerId FK
|
||||
string role
|
||||
string mime
|
||||
string title
|
||||
string blobId FK
|
||||
}
|
||||
|
||||
Blob {
|
||||
string blobId PK
|
||||
binary content
|
||||
string dateModified
|
||||
}
|
||||
|
||||
Option {
|
||||
string name PK
|
||||
string value
|
||||
boolean isSynced
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Definitions
|
||||
|
||||
### BNote - Notes with Content and Metadata
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/bnote.ts`
|
||||
|
||||
Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BNote {
|
||||
noteId: string; // Unique identifier
|
||||
title: string; // Display title
|
||||
type: string; // Content type (text, code, file, etc.)
|
||||
mime: string; // MIME type for content
|
||||
isProtected: boolean; // Encryption flag
|
||||
dateCreated: string; // Creation timestamp
|
||||
dateModified: string; // Last modification
|
||||
utcDateCreated: string; // UTC creation
|
||||
utcDateModified: string; // UTC modification
|
||||
|
||||
// Relationships
|
||||
parentBranches: BBranch[]; // Parent connections
|
||||
children: BBranch[]; // Child connections
|
||||
attributes: BAttribute[]; // Metadata
|
||||
|
||||
// Content
|
||||
content?: string | Buffer; // Note content (lazy loaded)
|
||||
|
||||
// Computed
|
||||
isDecrypted: boolean; // Decryption status
|
||||
}
|
||||
```
|
||||
|
||||
#### Note Types
|
||||
|
||||
* **text**: Rich text content with HTML formatting
|
||||
* **code**: Source code with syntax highlighting
|
||||
* **file**: Binary file attachment
|
||||
* **image**: Image with preview capabilities
|
||||
* **search**: Saved search query
|
||||
* **book**: Container for hierarchical documentation
|
||||
* **relationMap**: Visual relationship diagram
|
||||
* **canvas**: Drawing canvas (Excalidraw)
|
||||
* **mermaid**: Mermaid diagram
|
||||
* **mindMap**: Mind mapping visualization
|
||||
* **webView**: Embedded web content
|
||||
* **noteMap**: Tree visualization
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Create a new note
|
||||
const note = new BNote({
|
||||
noteId: generateNoteId(),
|
||||
title: "My Note",
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
content: "<p>Note content</p>"
|
||||
});
|
||||
note.save();
|
||||
|
||||
// Get note with content
|
||||
const note = becca.getNote(noteId);
|
||||
await note.loadContent();
|
||||
|
||||
// Update note
|
||||
note.title = "Updated Title";
|
||||
note.save();
|
||||
|
||||
// Protect note
|
||||
note.isProtected = true;
|
||||
note.encrypt();
|
||||
note.save();
|
||||
```
|
||||
|
||||
### BBranch - Hierarchical Relationships
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/bbranch.ts`
|
||||
|
||||
Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning).
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BBranch {
|
||||
branchId: string; // Unique identifier
|
||||
noteId: string; // Child note ID
|
||||
parentNoteId: string; // Parent note ID
|
||||
notePosition: number; // Order among siblings
|
||||
prefix: string; // Optional prefix label
|
||||
isExpanded: boolean; // Tree UI state
|
||||
|
||||
// Computed
|
||||
childNote: BNote; // Reference to child
|
||||
parentNote: BNote; // Reference to parent
|
||||
}
|
||||
```
|
||||
|
||||
#### Key Features
|
||||
|
||||
* **Multiple Parents**: Notes can appear in multiple locations
|
||||
* **Ordering**: Explicit positioning among siblings
|
||||
* **Prefixes**: Optional labels for context (e.g., "Chapter 1:")
|
||||
* **UI State**: Expansion state persisted per branch
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Create parent-child relationship
|
||||
const branch = new BBranch({
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: parentNote.noteId,
|
||||
notePosition: 10
|
||||
});
|
||||
branch.save();
|
||||
|
||||
// Clone note to another parent
|
||||
const cloneBranch = childNote.cloneTo(otherParent.noteId);
|
||||
|
||||
// Reorder children
|
||||
parentNote.sortChildren((a, b) =>
|
||||
a.title.localeCompare(b.title)
|
||||
);
|
||||
|
||||
// Add prefix
|
||||
branch.prefix = "Important: ";
|
||||
branch.save();
|
||||
```
|
||||
|
||||
### BAttribute - Key-Value Metadata
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/battribute.ts`
|
||||
|
||||
Attributes provide flexible metadata and relationships between notes.
|
||||
|
||||
#### Types
|
||||
|
||||
1. **Labels**: Key-value pairs for metadata
|
||||
2. **Relations**: References to other notes
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BAttribute {
|
||||
attributeId: string; // Unique identifier
|
||||
noteId: string; // Owning note
|
||||
type: 'label' | 'relation';
|
||||
name: string; // Attribute name
|
||||
value: string; // Value or target noteId
|
||||
position: number; // Display order
|
||||
isInheritable: boolean; // Inherited by children
|
||||
|
||||
// Computed
|
||||
note: BNote; // Owner note
|
||||
targetNote?: BNote; // For relations
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Patterns
|
||||
|
||||
```typescript
|
||||
// Add label
|
||||
note.addLabel("status", "active");
|
||||
note.addLabel("priority", "high");
|
||||
|
||||
// Add relation
|
||||
note.addRelation("template", templateNoteId);
|
||||
note.addRelation("renderNote", renderNoteId);
|
||||
|
||||
// Query by attributes
|
||||
const todos = becca.findAttributes("label", "todoItem");
|
||||
const templates = becca.findAttributes("label", "template");
|
||||
|
||||
// Inheritable attributes
|
||||
note.addLabel("workspace", "project", true); // Children inherit
|
||||
```
|
||||
|
||||
#### System Attributes
|
||||
|
||||
Special attributes with system behavior:
|
||||
|
||||
* `#hidePromotedAttributes`: Hide promoted attributes in UI
|
||||
* `#readOnly`: Prevent note editing
|
||||
* `#autoReadOnlyDisabled`: Disable auto read-only
|
||||
* `#hideChildrenOverview`: Hide children count
|
||||
* `~template`: Note template relation
|
||||
* `~renderNote`: Custom rendering relation
|
||||
|
||||
### BRevision - Version History
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/brevision.ts`
|
||||
|
||||
Revisions provide version history and recovery capabilities.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BRevision {
|
||||
revisionId: string; // Unique identifier
|
||||
noteId: string; // Parent note
|
||||
type: string; // Content type
|
||||
mime: string; // MIME type
|
||||
title: string; // Historical title
|
||||
isProtected: boolean; // Encryption flag
|
||||
dateCreated: string; // Creation time
|
||||
utcDateCreated: string; // UTC time
|
||||
dateModified: string; // Content modification
|
||||
blobId: string; // Content storage
|
||||
|
||||
// Methods
|
||||
getContent(): string | Buffer;
|
||||
restore(): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### Revision Strategy
|
||||
|
||||
* Created automatically on significant changes
|
||||
* Configurable retention period
|
||||
* Day/week/month/year retention rules
|
||||
* Protected note revisions are encrypted
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Get note revisions
|
||||
const revisions = note.getRevisions();
|
||||
|
||||
// Restore revision
|
||||
const revision = becca.getRevision(revisionId);
|
||||
revision.restore();
|
||||
|
||||
// Manual revision creation
|
||||
note.saveRevision();
|
||||
|
||||
// Compare revisions
|
||||
const diff = revision1.getContent() !== revision2.getContent();
|
||||
```
|
||||
|
||||
### BOption - Application Configuration
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/boption.ts`
|
||||
|
||||
Options store application and user preferences.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BOption {
|
||||
name: string; // Option key
|
||||
value: string; // Option value
|
||||
isSynced: boolean; // Sync across instances
|
||||
utcDateModified: string; // Last change
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Options
|
||||
|
||||
```typescript
|
||||
// Theme settings
|
||||
setOption("theme", "dark");
|
||||
|
||||
// Protected session timeout
|
||||
setOption("protectedSessionTimeout", "600");
|
||||
|
||||
// Sync settings
|
||||
setOption("syncServerHost", "https://sync.server");
|
||||
|
||||
// Note settings
|
||||
setOption("defaultNoteType", "text");
|
||||
```
|
||||
|
||||
### BAttachment - File Attachments
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/battachment.ts`
|
||||
|
||||
Attachments link binary content to notes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BAttachment {
|
||||
attachmentId: string; // Unique identifier
|
||||
ownerId: string; // Parent note ID
|
||||
role: string; // Attachment role
|
||||
mime: string; // MIME type
|
||||
title: string; // Display title
|
||||
blobId: string; // Content reference
|
||||
utcDateScheduledForDeletion: string;
|
||||
|
||||
// Methods
|
||||
getContent(): Buffer;
|
||||
getBlob(): BBlob;
|
||||
}
|
||||
```
|
||||
|
||||
#### Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Add attachment to note
|
||||
const attachment = note.addAttachment({
|
||||
role: "file",
|
||||
mime: "application/pdf",
|
||||
title: "document.pdf",
|
||||
content: buffer
|
||||
});
|
||||
|
||||
// Get attachments
|
||||
const attachments = note.getAttachments();
|
||||
|
||||
// Download attachment
|
||||
const content = attachment.getContent();
|
||||
```
|
||||
|
||||
## Entity Relationships
|
||||
|
||||
### Parent-Child Hierarchy
|
||||
|
||||
```typescript
|
||||
// Single parent
|
||||
childNote.setParent(parentNote.noteId);
|
||||
|
||||
// Multiple parents (cloning)
|
||||
childNote.cloneTo(parent1.noteId);
|
||||
childNote.cloneTo(parent2.noteId);
|
||||
|
||||
// Get parents
|
||||
const parents = childNote.getParentNotes();
|
||||
|
||||
// Get children
|
||||
const children = parentNote.getChildNotes();
|
||||
|
||||
// Get subtree
|
||||
const subtree = parentNote.getSubtreeNotes();
|
||||
```
|
||||
|
||||
### Attribute Relationships
|
||||
|
||||
```typescript
|
||||
// Direct relations
|
||||
note.addRelation("author", authorNote.noteId);
|
||||
|
||||
// Bidirectional relations
|
||||
note1.addRelation("related", note2.noteId);
|
||||
note2.addRelation("related", note1.noteId);
|
||||
|
||||
// Get related notes
|
||||
const related = note.getRelations("related");
|
||||
|
||||
// Get notes relating to this one
|
||||
const targetRelations = note.getTargetRelations();
|
||||
```
|
||||
|
||||
## Entity Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
```typescript
|
||||
// Note creation
|
||||
const note = new BNote({
|
||||
noteId: generateNoteId(),
|
||||
title: "New Note",
|
||||
type: "text"
|
||||
});
|
||||
note.save();
|
||||
|
||||
// With parent
|
||||
const child = parentNote.addChild({
|
||||
title: "Child Note",
|
||||
type: "text",
|
||||
content: "Content"
|
||||
});
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
```typescript
|
||||
// Atomic updates
|
||||
note.title = "New Title";
|
||||
note.save();
|
||||
|
||||
// Batch updates
|
||||
sql.transactional(() => {
|
||||
note1.title = "Title 1";
|
||||
note1.save();
|
||||
|
||||
note2.content = "Content 2";
|
||||
note2.save();
|
||||
});
|
||||
```
|
||||
|
||||
### Deletion
|
||||
|
||||
```typescript
|
||||
// Soft delete (move to trash)
|
||||
note.deleteNote();
|
||||
|
||||
// Mark for deletion
|
||||
note.isDeleted = true;
|
||||
note.save();
|
||||
|
||||
// Permanent deletion (after grace period)
|
||||
note.eraseNote();
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
// Note content loaded on demand
|
||||
const note = becca.getNote(noteId); // Metadata only
|
||||
await note.loadContent(); // Load content when needed
|
||||
|
||||
// Revisions loaded on demand
|
||||
const revisions = note.getRevisions(); // Database query
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Efficient bulk loading
|
||||
const notes = becca.getNotes(noteIds);
|
||||
|
||||
// Batch attribute queries
|
||||
const attributes = sql.getRows(`
|
||||
SELECT * FROM attributes
|
||||
WHERE noteId IN (???)
|
||||
AND name = ?
|
||||
`, [noteIds, 'label']);
|
||||
```
|
||||
|
||||
### Indexing
|
||||
|
||||
```typescript
|
||||
// Attribute index for fast lookups
|
||||
const labels = becca.findAttributes("label", "important");
|
||||
|
||||
// Branch index for relationship queries
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Entity Creation
|
||||
|
||||
```typescript
|
||||
// Always use transactions for multiple operations
|
||||
sql.transactional(() => {
|
||||
const note = new BNote({...});
|
||||
note.save();
|
||||
|
||||
note.addLabel("status", "draft");
|
||||
note.addRelation("template", templateId);
|
||||
});
|
||||
```
|
||||
|
||||
### Entity Updates
|
||||
|
||||
```typescript
|
||||
// Check existence before update
|
||||
const note = becca.getNote(noteId);
|
||||
if (note) {
|
||||
note.title = "Updated";
|
||||
note.save();
|
||||
}
|
||||
|
||||
// Use proper error handling
|
||||
try {
|
||||
const note = becca.getNoteOrThrow(noteId);
|
||||
note.save();
|
||||
} catch (e) {
|
||||
log.error(`Note ${noteId} not found`);
|
||||
}
|
||||
```
|
||||
|
||||
### Querying
|
||||
|
||||
```typescript
|
||||
// Use indexed queries
|
||||
const attrs = becca.findAttributes("label", "task");
|
||||
|
||||
// Avoid N+1 queries
|
||||
const noteIds = [...];
|
||||
const notes = becca.getNotes(noteIds); // Single batch
|
||||
|
||||
// Use SQL for complex queries
|
||||
const results = sql.getRows(`
|
||||
SELECT n.noteId, n.title, COUNT(b.branchId) as childCount
|
||||
FROM notes n
|
||||
LEFT JOIN branches b ON b.parentNoteId = n.noteId
|
||||
GROUP BY n.noteId
|
||||
`);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Circular References**
|
||||
|
||||
```typescript
|
||||
// Detect cycles before creating branches
|
||||
if (!parentNote.hasAncestor(childNote.noteId)) {
|
||||
childNote.setParent(parentNote.noteId);
|
||||
}
|
||||
```
|
||||
2. **Orphaned Entities**
|
||||
|
||||
```typescript
|
||||
// Find orphaned notes
|
||||
const orphans = sql.getRows(`
|
||||
SELECT noteId FROM notes
|
||||
WHERE noteId != 'root'
|
||||
AND noteId NOT IN (SELECT noteId FROM branches)
|
||||
`);
|
||||
```
|
||||
3. **Attribute Conflicts**
|
||||
|
||||
```typescript
|
||||
// Handle duplicate attributes
|
||||
const existing = note.getAttribute("label", "status");
|
||||
if (existing) {
|
||||
existing.value = "new value";
|
||||
existing.save();
|
||||
} else {
|
||||
note.addLabel("status", "new value");
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
|
||||
* [Database Schema](#root/eZcnGfMUmic0) - Database structure
|
||||
* [Script API](#root/7Pp4moCrBVzA) - Entity API for scripts
|
||||
612
docs/Developer Guide/Developer Guide/Architecture/Monorepo-Structure.md
vendored
Normal file
612
docs/Developer Guide/Developer Guide/Architecture/Monorepo-Structure.md
vendored
Normal file
@@ -0,0 +1,612 @@
|
||||
# Monorepo-Structure
|
||||
## Monorepo Structure
|
||||
|
||||
Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes. This document provides a comprehensive overview of the project structure, build system, and development workflow.
|
||||
|
||||
## Project Organization
|
||||
|
||||
```
|
||||
TriliumNext/Trilium/
|
||||
├── apps/ # Runnable applications
|
||||
│ ├── client/ # Frontend web application
|
||||
│ ├── server/ # Node.js backend server
|
||||
│ ├── desktop/ # Electron desktop application
|
||||
│ ├── web-clipper/ # Browser extension
|
||||
│ ├── db-compare/ # Database comparison tool
|
||||
│ ├── dump-db/ # Database dump utility
|
||||
│ └── edit-docs/ # Documentation editor
|
||||
├── packages/ # Shared libraries
|
||||
│ ├── commons/ # Shared interfaces and utilities
|
||||
│ ├── ckeditor5/ # Rich text editor
|
||||
│ ├── codemirror/ # Code editor
|
||||
│ ├── highlightjs/ # Syntax highlighting
|
||||
│ ├── ckeditor5-admonition/ # CKEditor plugin
|
||||
│ ├── ckeditor5-footnotes/ # CKEditor plugin
|
||||
│ ├── ckeditor5-math/ # CKEditor plugin
|
||||
│ └── ckeditor5-mermaid/ # CKEditor plugin
|
||||
├── docs/ # Documentation
|
||||
├── nx.json # NX workspace configuration
|
||||
├── package.json # Root package configuration
|
||||
├── pnpm-workspace.yaml # PNPM workspace configuration
|
||||
└── tsconfig.base.json # Base TypeScript configuration
|
||||
```
|
||||
|
||||
## Applications
|
||||
|
||||
### Client (`/apps/client`)
|
||||
|
||||
The frontend application shared by both server and desktop versions.
|
||||
|
||||
```
|
||||
apps/client/
|
||||
├── src/
|
||||
│ ├── components/ # Core UI components
|
||||
│ ├── entities/ # Frontend entities (FNote, FBranch, etc.)
|
||||
│ ├── services/ # Business logic and API calls
|
||||
│ ├── widgets/ # UI widgets system
|
||||
│ │ ├── type_widgets/ # Note type specific widgets
|
||||
│ │ ├── dialogs/ # Dialog components
|
||||
│ │ └── panels/ # Panel widgets
|
||||
│ ├── public/
|
||||
│ │ ├── fonts/ # Font assets
|
||||
│ │ ├── images/ # Image assets
|
||||
│ │ └── libraries/ # Third-party libraries
|
||||
│ └── desktop.ts # Desktop entry point
|
||||
├── package.json
|
||||
├── project.json # NX project configuration
|
||||
└── vite.config.ts # Vite build configuration
|
||||
```
|
||||
|
||||
#### Key Files
|
||||
|
||||
* `desktop.ts` - Main application initialization
|
||||
* `services/froca.ts` - Frontend cache implementation
|
||||
* `widgets/basic_widget.ts` - Base widget class
|
||||
* `services/server.ts` - API communication layer
|
||||
|
||||
### Server (`/apps/server`)
|
||||
|
||||
The Node.js backend providing API, database, and business logic.
|
||||
|
||||
```
|
||||
apps/server/
|
||||
├── src/
|
||||
│ ├── becca/ # Backend cache system
|
||||
│ │ ├── entities/ # Core entities (BNote, BBranch, etc.)
|
||||
│ │ └── becca.ts # Cache interface
|
||||
│ ├── routes/ # Express routes
|
||||
│ │ ├── api/ # Internal API endpoints
|
||||
│ │ └── pages/ # HTML page routes
|
||||
│ ├── etapi/ # External API
|
||||
│ ├── services/ # Business services
|
||||
│ ├── share/ # Note sharing functionality
|
||||
│ │ └── shaca/ # Share cache
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ ├── assets/
|
||||
│ │ ├── db/ # Database schema
|
||||
│ │ └── doc_notes/ # Documentation notes
|
||||
│ └── main.ts # Server entry point
|
||||
├── package.json
|
||||
├── project.json
|
||||
└── webpack.config.js # Webpack configuration
|
||||
```
|
||||
|
||||
#### Key Services
|
||||
|
||||
* `services/sql.ts` - Database access layer
|
||||
* `services/sync.ts` - Synchronization logic
|
||||
* `services/ws.ts` - WebSocket server
|
||||
* `services/protected_session.ts` - Encryption handling
|
||||
|
||||
### Desktop (`/apps/desktop`)
|
||||
|
||||
Electron wrapper for the desktop application.
|
||||
|
||||
```
|
||||
apps/desktop/
|
||||
├── src/
|
||||
│ ├── main.ts # Electron main process
|
||||
│ ├── preload.ts # Preload script
|
||||
│ ├── services/ # Desktop-specific services
|
||||
│ └── utils/ # Desktop utilities
|
||||
├── resources/ # Desktop resources (icons, etc.)
|
||||
├── package.json
|
||||
└── electron-builder.yml # Electron Builder configuration
|
||||
```
|
||||
|
||||
### Web Clipper (`/apps/web-clipper`)
|
||||
|
||||
Browser extension for saving web content to Trilium.
|
||||
|
||||
```
|
||||
apps/web-clipper/
|
||||
├── src/
|
||||
│ ├── background.js # Background script
|
||||
│ ├── content.js # Content script
|
||||
│ ├── popup/ # Extension popup
|
||||
│ └── options/ # Extension options
|
||||
├── manifest.json # Extension manifest
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### Commons (`/packages/commons`)
|
||||
|
||||
Shared TypeScript interfaces and utilities used across applications.
|
||||
|
||||
```typescript
|
||||
// packages/commons/src/types.ts
|
||||
export interface NoteRow {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
isProtected: boolean;
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
}
|
||||
|
||||
export interface BranchRow {
|
||||
branchId: string;
|
||||
noteId: string;
|
||||
parentNoteId: string;
|
||||
notePosition: number;
|
||||
prefix: string;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### CKEditor5 (`/packages/ckeditor5`)
|
||||
|
||||
Custom CKEditor5 build with Trilium-specific plugins.
|
||||
|
||||
```
|
||||
packages/ckeditor5/
|
||||
├── src/
|
||||
│ ├── ckeditor.ts # Editor configuration
|
||||
│ ├── plugins.ts # Plugin registration
|
||||
│ └── trilium/ # Custom plugins
|
||||
├── theme/ # Editor themes
|
||||
└── package.json
|
||||
```
|
||||
|
||||
#### Custom Plugins
|
||||
|
||||
* **Admonition**: Note boxes with icons
|
||||
* **Footnotes**: Reference footnotes
|
||||
* **Math**: LaTeX equation rendering
|
||||
* **Mermaid**: Diagram integration
|
||||
|
||||
### CodeMirror (`/packages/codemirror`)
|
||||
|
||||
Code editor customizations for the code note type.
|
||||
|
||||
```typescript
|
||||
// packages/codemirror/src/index.ts
|
||||
export function createCodeMirror(element: HTMLElement, options: CodeMirrorOptions) {
|
||||
return CodeMirror(element, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
// Trilium-specific customizations
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
### NX Configuration
|
||||
|
||||
**`nx.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "test", "lint"],
|
||||
"parallel": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project Configuration
|
||||
|
||||
Each application and package has a `project.json` defining its targets:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "server",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/server",
|
||||
"main": "apps/server/src/main.ts",
|
||||
"tsConfig": "apps/server/tsconfig.app.json"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/node:node",
|
||||
"options": {
|
||||
"buildTarget": "server:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"options": {
|
||||
"jestConfig": "apps/server/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
```sh
|
||||
# Build specific project
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
|
||||
# Build all projects
|
||||
pnpm nx run-many --target=build --all
|
||||
|
||||
# Build with dependencies
|
||||
pnpm nx build server --with-deps
|
||||
|
||||
# Production build
|
||||
pnpm nx build server --configuration=production
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Enable corepack for pnpm
|
||||
corepack enable
|
||||
|
||||
# Build all packages
|
||||
pnpm nx run-many --target=build --all
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
```sh
|
||||
# Start development server
|
||||
pnpm run server:start
|
||||
# or
|
||||
pnpm nx run server:serve
|
||||
|
||||
# Start desktop app
|
||||
pnpm nx run desktop:serve
|
||||
|
||||
# Run client dev server
|
||||
pnpm nx run client:serve
|
||||
|
||||
# Watch mode for packages
|
||||
pnpm nx run commons:build --watch
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```sh
|
||||
# Run all tests
|
||||
pnpm test:all
|
||||
|
||||
# Run tests for specific project
|
||||
pnpm nx test server
|
||||
pnpm nx test client
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm nx test server --watch
|
||||
|
||||
# Generate coverage
|
||||
pnpm nx test server --coverage
|
||||
```
|
||||
|
||||
### Linting and Type Checking
|
||||
|
||||
```sh
|
||||
# Lint specific project
|
||||
pnpm nx lint server
|
||||
|
||||
# Type check
|
||||
pnpm nx run server:typecheck
|
||||
|
||||
# Lint all projects
|
||||
pnpm nx run-many --target=lint --all
|
||||
|
||||
# Fix lint issues
|
||||
pnpm nx lint server --fix
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Package Dependencies
|
||||
|
||||
Dependencies are managed at both root and project levels:
|
||||
|
||||
```json
|
||||
// Root package.json - shared dev dependencies
|
||||
{
|
||||
"devDependencies": {
|
||||
"@nx/workspace": "^17.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
// Project package.json - project-specific dependencies
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^4.18.0",
|
||||
"better-sqlite3": "^9.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Dependencies
|
||||
|
||||
```sh
|
||||
# Add to root
|
||||
pnpm add -D typescript
|
||||
|
||||
# Add to specific project
|
||||
pnpm add express --filter server
|
||||
|
||||
# Add to multiple projects
|
||||
pnpm add lodash --filter server --filter client
|
||||
```
|
||||
|
||||
### Workspace References
|
||||
|
||||
Internal packages are referenced using workspace protocol:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@triliumnext/commons": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
### Base Configuration
|
||||
|
||||
**`tsconfig.base.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@triliumnext/commons": ["packages/commons/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project-Specific Configuration
|
||||
|
||||
```json
|
||||
// apps/server/tsconfig.json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
|
||||
### NX Cloud
|
||||
|
||||
```sh
|
||||
# Enable NX Cloud for distributed caching
|
||||
pnpm nx connect-to-nx-cloud
|
||||
```
|
||||
|
||||
### Affected Commands
|
||||
|
||||
```sh
|
||||
# Build only affected projects
|
||||
pnpm nx affected:build --base=main
|
||||
|
||||
# Test only affected projects
|
||||
pnpm nx affected:test --base=main
|
||||
|
||||
# Lint only affected projects
|
||||
pnpm nx affected:lint --base=main
|
||||
```
|
||||
|
||||
### Build Caching
|
||||
|
||||
NX caches build outputs to speed up subsequent builds:
|
||||
|
||||
```sh
|
||||
# Clear cache
|
||||
pnpm nx reset
|
||||
|
||||
# Run with cache disabled
|
||||
pnpm nx build server --skip-nx-cache
|
||||
|
||||
# See cache statistics
|
||||
pnpm nx report
|
||||
```
|
||||
|
||||
## Production Builds
|
||||
|
||||
### Building for Production
|
||||
|
||||
```sh
|
||||
# Build server for production
|
||||
pnpm nx build server --configuration=production
|
||||
|
||||
# Build client with optimization
|
||||
pnpm nx build client --configuration=production
|
||||
|
||||
# Build desktop app
|
||||
pnpm nx build desktop --configuration=production
|
||||
pnpm electron:build # Creates distributables
|
||||
```
|
||||
|
||||
### Docker Build
|
||||
|
||||
```dockerfile
|
||||
# Multi-stage build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json pnpm-lock.yaml ./
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm nx build server --configuration=production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist/apps/server ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
CMD ["node", "main.js"]
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- run: pnpm nx affected:lint --base=origin/main
|
||||
|
||||
- run: pnpm nx affected:test --base=origin/main
|
||||
|
||||
- run: pnpm nx affected:build --base=origin/main
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build Cache Issues**
|
||||
|
||||
```sh
|
||||
# Clear NX cache
|
||||
pnpm nx reset
|
||||
|
||||
# Clear node_modules and reinstall
|
||||
rm -rf node_modules
|
||||
pnpm install
|
||||
```
|
||||
2. **Dependency Version Conflicts**
|
||||
|
||||
```sh
|
||||
# Check for duplicate packages
|
||||
pnpm list --depth=0
|
||||
|
||||
# Update all dependencies
|
||||
pnpm update --recursive
|
||||
```
|
||||
3. **TypeScript Path Resolution**
|
||||
|
||||
```sh
|
||||
# Verify TypeScript paths
|
||||
pnpm nx run server:typecheck --traceResolution
|
||||
```
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```sh
|
||||
# Show project graph
|
||||
pnpm nx graph
|
||||
|
||||
# Show project dependencies
|
||||
pnpm nx print-affected --type=app --select=projects
|
||||
|
||||
# Verbose output
|
||||
pnpm nx build server --verbose
|
||||
|
||||
# Profile build performance
|
||||
pnpm nx build server --profile
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Project Structure
|
||||
|
||||
1. **Keep packages focused**: Each package should have a single, clear purpose
|
||||
2. **Minimize circular dependencies**: Use dependency graph to identify issues
|
||||
3. **Share common code**: Extract shared logic to packages/commons
|
||||
|
||||
### Development
|
||||
|
||||
1. **Use NX generators**: Generate consistent code structure
|
||||
2. **Leverage caching**: Don't skip-nx-cache unless debugging
|
||||
3. **Run affected commands**: Save time by only building/testing changed code
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Colocate tests**: Keep test files next to source files
|
||||
2. **Use workspace scripts**: Define common scripts in root package.json
|
||||
3. **Parallel execution**: Use `--parallel` flag for faster execution
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Environment Setup](#root/tFVKyUp99QEc) - Development environment setup
|
||||
* [Project Structure](#root/NurKhC1Zq1CN) - Detailed project structure
|
||||
* [Build Information](#root/m5GSI5gIyqs5) - Build details
|
||||
97
docs/Developer Guide/Developer Guide/Architecture/README.md
vendored
Normal file
97
docs/Developer Guide/Developer Guide/Architecture/README.md
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
# README
|
||||
## Trilium Architecture Documentation
|
||||
|
||||
This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Three-Layer Cache System](Three-Layer-Cache-System.md)
|
||||
2. [Entity System](Entity-System.md)
|
||||
3. [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
|
||||
4. [API Architecture](API-Architecture.md)
|
||||
5. [Monorepo Structure](Monorepo-Structure.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns:
|
||||
|
||||
* **Three-layer caching system** for optimal performance across backend, frontend, and shared content
|
||||
* **Entity-based data model** supporting hierarchical note structures with multiple parent relationships
|
||||
* **Widget-based UI architecture** enabling modular and extensible interface components
|
||||
* **Multiple API layers** for internal operations, external integrations, and real-time synchronization
|
||||
* **Monorepo structure** facilitating code sharing and consistent development patterns
|
||||
|
||||
## Quick Start for Developers
|
||||
|
||||
If you're new to Trilium development, start with these sections:
|
||||
|
||||
1. [Monorepo Structure](Monorepo-Structure.md) - Understand the project organization
|
||||
2. [Entity System](Entity-System.md) - Learn about the core data model
|
||||
3. [Three-Layer Cache System](Three-Layer-Cache-System.md) - Understand data flow and caching
|
||||
|
||||
For UI development, refer to:
|
||||
|
||||
* [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
|
||||
|
||||
For API integration, see:
|
||||
|
||||
* [API Architecture](API-Architecture.md)
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Performance First
|
||||
|
||||
* Lazy loading of note content
|
||||
* Efficient caching at multiple layers
|
||||
* Optimized database queries with prepared statements
|
||||
|
||||
### Flexibility
|
||||
|
||||
* Support for multiple note types
|
||||
* Extensible through scripting
|
||||
* Plugin architecture for UI widgets
|
||||
|
||||
### Data Integrity
|
||||
|
||||
* Transactional database operations
|
||||
* Revision history for all changes
|
||||
* Synchronization conflict resolution
|
||||
|
||||
### Security
|
||||
|
||||
* Per-note encryption
|
||||
* Protected sessions
|
||||
* API authentication tokens
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Setup Development Environment**
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
2. **Make Changes**
|
||||
|
||||
* Backend changes in `apps/server/src/`
|
||||
* Frontend changes in `apps/client/src/`
|
||||
* Shared code in `packages/`
|
||||
3. **Test Your Changes**
|
||||
|
||||
```sh
|
||||
pnpm test:all
|
||||
pnpm nx run <project>:lint
|
||||
```
|
||||
4. **Build for Production**
|
||||
|
||||
```sh
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
* [Development Environment Setup](#root/tFVKyUp99QEc)
|
||||
* [Adding a New Note Type](#root/6aV1LKciq0CF)
|
||||
* [Database Schema](#root/eZcnGfMUmic0)
|
||||
* [Script API Documentation](#root/7Pp4moCrBVzA)
|
||||
374
docs/Developer Guide/Developer Guide/Architecture/Three-Layer-Cache-System.md
vendored
Normal file
374
docs/Developer Guide/Developer Guide/Architecture/Three-Layer-Cache-System.md
vendored
Normal file
@@ -0,0 +1,374 @@
|
||||
# Three-Layer-Cache-System
|
||||
## Three-Layer Cache System Architecture
|
||||
|
||||
Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts.
|
||||
|
||||
## Overview
|
||||
|
||||
The three cache layers are:
|
||||
|
||||
1. **Becca** (Backend Cache) - Server-side entity cache
|
||||
2. **Froca** (Frontend Cache) - Client-side mirror of backend data
|
||||
3. **Shaca** (Share Cache) - Optimized cache for shared/published notes
|
||||
|
||||
```
|
||||
graph TB
|
||||
subgraph "Database Layer"
|
||||
DB[(SQLite Database)]
|
||||
end
|
||||
|
||||
subgraph "Backend Layer"
|
||||
Becca[Becca Cache<br/>Backend Cache]
|
||||
API[API Layer]
|
||||
end
|
||||
|
||||
subgraph "Frontend Layer"
|
||||
Froca[Froca Cache<br/>Frontend Cache]
|
||||
UI[UI Components]
|
||||
end
|
||||
|
||||
subgraph "Share Layer"
|
||||
Shaca[Shaca Cache<br/>Share Cache]
|
||||
Share[Public Share Interface]
|
||||
end
|
||||
|
||||
DB <--> Becca
|
||||
Becca <--> API
|
||||
API <--> Froca
|
||||
Froca <--> UI
|
||||
DB <--> Shaca
|
||||
Shaca <--> Share
|
||||
|
||||
style Becca fill:#e1f5fe
|
||||
style Froca fill:#fff3e0
|
||||
style Shaca fill:#f3e5f5
|
||||
```
|
||||
|
||||
## Becca (Backend Cache)
|
||||
|
||||
**Location**: `/apps/server/src/becca/`
|
||||
|
||||
Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Becca Interface (`becca-interface.ts`)
|
||||
|
||||
```typescript
|
||||
export default class Becca {
|
||||
loaded: boolean;
|
||||
notes: Record<string, BNote>;
|
||||
branches: Record<string, BBranch>;
|
||||
childParentToBranch: Record<string, BBranch>;
|
||||
attributes: Record<string, BAttribute>;
|
||||
attributeIndex: Record<string, BAttribute[]>;
|
||||
options: Record<string, BOption>;
|
||||
etapiTokens: Record<string, BEtapiToken>;
|
||||
allNoteSetCache: NoteSet | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
* **In-memory storage**: All active entities are kept in memory for fast access
|
||||
* **Lazy loading**: Related entities (revisions, attachments) loaded on demand
|
||||
* **Index structures**: Optimized lookups via `childParentToBranch` and `attributeIndex`
|
||||
* **Cache invalidation**: Automatic cache updates on entity changes
|
||||
* **Protected note decryption**: On-demand decryption of encrypted content
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import becca from "./becca/becca.js";
|
||||
|
||||
// Get a note
|
||||
const note = becca.getNote("noteId");
|
||||
|
||||
// Find attributes by type and name
|
||||
const labels = becca.findAttributes("label", "todoItem");
|
||||
|
||||
// Get branch relationships
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Initialization**: Load all notes, branches, and attributes from database
|
||||
2. **Access**: Direct memory access for cached entities
|
||||
3. **Updates**: Write-through cache with immediate database persistence
|
||||
4. **Invalidation**: Automatic cache refresh on entity changes
|
||||
|
||||
## Froca (Frontend Cache)
|
||||
|
||||
**Location**: `/apps/client/src/services/froca.ts`
|
||||
|
||||
Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Froca Implementation (`froca.ts`)
|
||||
|
||||
```typescript
|
||||
class FrocaImpl implements Froca {
|
||||
notes: Record<string, FNote>;
|
||||
branches: Record<string, FBranch>;
|
||||
attributes: Record<string, FAttribute>;
|
||||
attachments: Record<string, FAttachment>;
|
||||
blobPromises: Record<string, Promise<FBlob | null> | null>;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
* **Lazy loading**: Notes loaded on-demand with their immediate context
|
||||
* **Subtree loading**: Efficient loading of note hierarchies
|
||||
* **Real-time updates**: WebSocket synchronization with backend changes
|
||||
* **Search note support**: Virtual branches for search results
|
||||
* **Promise-based blob loading**: Asynchronous content loading
|
||||
|
||||
### Loading Strategy
|
||||
|
||||
```typescript
|
||||
// Initial load - loads root and immediate children
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Load subtree on demand
|
||||
const note = await froca.loadSubTree(noteId);
|
||||
|
||||
// Reload specific notes
|
||||
await froca.reloadNotes([noteId1, noteId2]);
|
||||
```
|
||||
|
||||
### Synchronization
|
||||
|
||||
Froca maintains consistency with Becca through:
|
||||
|
||||
1. **Initial sync**: Load essential tree structure on startup
|
||||
2. **On-demand loading**: Fetch notes as needed
|
||||
3. **WebSocket updates**: Real-time push of changes from backend
|
||||
4. **Batch reloading**: Efficient refresh of multiple notes
|
||||
|
||||
## Shaca (Share Cache)
|
||||
|
||||
**Location**: `/apps/server/src/share/shaca/`
|
||||
|
||||
Shaca is a specialized cache for publicly shared notes, optimized for read-only access.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Shaca Interface (`shaca-interface.ts`)
|
||||
|
||||
```typescript
|
||||
export default class Shaca {
|
||||
notes: Record<string, SNote>;
|
||||
branches: Record<string, SBranch>;
|
||||
childParentToBranch: Record<string, SBranch>;
|
||||
attributes: Record<string, SAttribute>;
|
||||
attachments: Record<string, SAttachment>;
|
||||
aliasToNote: Record<string, SNote>;
|
||||
shareRootNote: SNote | null;
|
||||
shareIndexEnabled: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
* **Read-only optimization**: Streamlined for public access
|
||||
* **Alias support**: URL-friendly note access via aliases
|
||||
* **Share index**: Optional indexing of all shared subtrees
|
||||
* **Minimal memory footprint**: Only shared content cached
|
||||
* **Security isolation**: Separate from main application cache
|
||||
|
||||
### Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Get shared note by ID
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
// Access via alias
|
||||
const aliasedNote = shaca.aliasToNote[alias];
|
||||
|
||||
// Check if note is shared
|
||||
const isShared = shaca.hasNote(noteId);
|
||||
```
|
||||
|
||||
## Cache Interaction and Data Flow
|
||||
|
||||
### 1\. Create/Update Flow
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Froca
|
||||
participant API
|
||||
participant Becca
|
||||
participant DB
|
||||
|
||||
Client->>API: Update Note
|
||||
API->>Becca: Update Cache
|
||||
Becca->>DB: Persist Change
|
||||
Becca->>API: Confirm
|
||||
API->>Froca: Push Update (WebSocket)
|
||||
Froca->>Client: Update UI
|
||||
```
|
||||
|
||||
### 2\. Read Flow
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Froca
|
||||
participant API
|
||||
participant Becca
|
||||
|
||||
Client->>Froca: Request Note
|
||||
alt Note in Cache
|
||||
Froca->>Client: Return Cached Note
|
||||
else Note not in Cache
|
||||
Froca->>API: Fetch Note
|
||||
API->>Becca: Get Note
|
||||
Becca->>API: Return Note
|
||||
API->>Froca: Send Note Data
|
||||
Froca->>Froca: Cache Note
|
||||
Froca->>Client: Return Note
|
||||
end
|
||||
```
|
||||
|
||||
### 3\. Share Access Flow
|
||||
|
||||
```
|
||||
sequenceDiagram
|
||||
participant Browser
|
||||
participant ShareUI
|
||||
participant Shaca
|
||||
participant DB
|
||||
|
||||
Browser->>ShareUI: Access Shared URL
|
||||
ShareUI->>Shaca: Get Shared Note
|
||||
alt Note in Cache
|
||||
Shaca->>ShareUI: Return Cached
|
||||
else Not in Cache
|
||||
Shaca->>DB: Load Shared Tree
|
||||
DB->>Shaca: Return Data
|
||||
Shaca->>Shaca: Build Cache
|
||||
Shaca->>ShareUI: Return Note
|
||||
end
|
||||
ShareUI->>Browser: Render Content
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Management
|
||||
|
||||
* **Becca**: Keeps entire note tree in memory (~100-500MB for typical use)
|
||||
* **Froca**: Loads notes on-demand, automatic cleanup of unused notes
|
||||
* **Shaca**: Minimal footprint, only shared content
|
||||
|
||||
### Cache Warming
|
||||
|
||||
* **Becca**: Full load on server startup
|
||||
* **Froca**: Progressive loading based on user navigation
|
||||
* **Shaca**: Lazy loading with configurable index
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Attribute Indexing**: Pre-built indexes for fast attribute queries
|
||||
2. **Batch Operations**: Group updates to minimize round trips
|
||||
3. **Partial Loading**: Load only required fields for lists
|
||||
4. **WebSocket Compression**: Compressed real-time updates
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Each Cache
|
||||
|
||||
**Use Becca when**:
|
||||
|
||||
* Implementing server-side business logic
|
||||
* Performing bulk operations
|
||||
* Handling synchronization
|
||||
* Managing protected notes
|
||||
|
||||
**Use Froca when**:
|
||||
|
||||
* Building UI components
|
||||
* Handling user interactions
|
||||
* Displaying note content
|
||||
* Managing client state
|
||||
|
||||
**Use Shaca when**:
|
||||
|
||||
* Serving public content
|
||||
* Building share pages
|
||||
* Implementing read-only access
|
||||
* Creating public APIs
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
```typescript
|
||||
// Becca - automatic on entity save
|
||||
note.save(); // Cache updated automatically
|
||||
|
||||
// Froca - manual reload when needed
|
||||
await froca.reloadNotes([noteId]);
|
||||
|
||||
// Shaca - rebuild on share changes
|
||||
shaca.reset();
|
||||
shaca.load();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// Becca - throw on missing required entities
|
||||
const note = becca.getNoteOrThrow(noteId); // throws NotFoundError
|
||||
|
||||
// Froca - graceful degradation
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
// Handle missing note
|
||||
}
|
||||
|
||||
// Shaca - check existence first
|
||||
if (shaca.hasNote(noteId)) {
|
||||
const note = shaca.getNote(noteId);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Cache Inconsistency**
|
||||
|
||||
* Symptom: UI shows outdated data
|
||||
* Solution: Force reload with `froca.reloadNotes()`
|
||||
2. **Memory Growth**
|
||||
|
||||
* Symptom: Server memory usage increases
|
||||
* Solution: Check for memory leaks in custom scripts
|
||||
3. **Slow Initial Load**
|
||||
|
||||
* Symptom: Long startup time
|
||||
* Solution: Optimize database queries, add indexes
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```javascript
|
||||
// Check cache sizes
|
||||
console.log('Becca notes:', Object.keys(becca.notes).length);
|
||||
console.log('Froca notes:', Object.keys(froca.notes).length);
|
||||
console.log('Shaca notes:', Object.keys(shaca.notes).length);
|
||||
|
||||
// Force cache refresh
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Clear and reload Shaca
|
||||
shaca.reset();
|
||||
await shaca.load();
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Entity System](Entity-System.md) - Detailed entity documentation
|
||||
* [Database Schema](#root/eZcnGfMUmic0) - Database structure
|
||||
* [WebSocket Synchronization](#root/UBgXVlHv3d66) - Real-time updates
|
||||
514
docs/Developer Guide/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
vendored
Normal file
514
docs/Developer Guide/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
vendored
Normal file
@@ -0,0 +1,514 @@
|
||||
# Widget-Based-UI-Architecture
|
||||
## Widget-Based UI Architecture
|
||||
|
||||
Trilium's user interface is built on a widget system where each UI element is a self-contained component. This approach makes the interface modular, allowing developers to create custom widgets and extend the application's functionality.
|
||||
|
||||
## Understanding Widgets
|
||||
|
||||
Widgets in Trilium form a hierarchy, with each level adding specific capabilities. At the base, every widget is a Component that can have children and communicate with other widgets. The BasicWidget layer adds DOM manipulation, while NoteContextAwareWidget provides automatic updates when notes change.
|
||||
|
||||
## Core Widget Classes
|
||||
|
||||
### Component (Base Class)
|
||||
|
||||
**Location**: `/apps/client/src/components/component.js`
|
||||
|
||||
Every UI element in Trilium inherits from Component, which provides the foundation for parent-child relationships and event communication between widgets.
|
||||
|
||||
### BasicWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/basic_widget.ts`
|
||||
|
||||
BasicWidget extends Component with DOM manipulation capabilities. It provides a fluent API for styling and composing UI elements:
|
||||
|
||||
```typescript
|
||||
class MyWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="my-widget">');
|
||||
this.$widget.append($('<h3>').text('Title'));
|
||||
return this.$widget;
|
||||
}
|
||||
}
|
||||
|
||||
// Compose widgets using method chaining
|
||||
const container = new FlexContainer('column')
|
||||
.id('main-container')
|
||||
.css('padding', '10px')
|
||||
.child(new MyWidget());
|
||||
```
|
||||
|
||||
The chaining API makes it easy to build complex UIs declaratively. Methods like `css()`, `class()`, and `child()` return the widget itself, allowing you to chain multiple operations together.
|
||||
|
||||
### NoteContextAwareWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/note_context_aware_widget.ts`
|
||||
|
||||
Widgets that need to react to note changes extend NoteContextAwareWidget. This class automatically calls your widget's methods when the active note changes, making it easy to keep the UI synchronized with the current note.
|
||||
|
||||
```typescript
|
||||
class MyNoteWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
// Automatically called when note changes
|
||||
this.$widget.find('.title').text(note.title);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key lifecycle methods:
|
||||
|
||||
* `refreshWithNote(note)` - Called when the active note changes
|
||||
* `noteSwitched()` - Called after switching to a different note
|
||||
* `noteTypeMimeChanged()` - Called when the note's type changes
|
||||
|
||||
The widget automatically maintains references to the current note, making it simple to build note-aware UI components.
|
||||
|
||||
### RightPanelWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/right_panel_widget.ts`
|
||||
|
||||
Widgets in the right sidebar extend RightPanelWidget. These widgets appear as collapsible panels and can show note-specific information or tools.
|
||||
|
||||
```typescript
|
||||
class InfoWidget extends RightPanelWidget {
|
||||
getTitle() { return "Note Info"; }
|
||||
getIcon() { return "info"; }
|
||||
|
||||
async doRenderBody() {
|
||||
return $('<div class="info-widget">');
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$body.find('.created').text(`Created: ${note.dateCreated}`);
|
||||
this.$body.find('.modified').text(`Modified: ${note.dateModified}`);
|
||||
|
||||
const wordCount = this.calculateWordCount(await note.getContent());
|
||||
this.$body.find('.word-count').text(`Words: ${wordCount}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Specific Widgets
|
||||
|
||||
**Location**: `/apps/client/src/widgets/type_widgets/`
|
||||
|
||||
Each note type has a specialized widget for rendering and editing.
|
||||
|
||||
### TypeWidget Interface
|
||||
|
||||
```typescript
|
||||
abstract class TypeWidget extends NoteContextAwareWidget {
|
||||
abstract static getType(): string;
|
||||
|
||||
// Content management
|
||||
async getContent(): Promise<string>;
|
||||
async saveContent(content: string): Promise<void>;
|
||||
|
||||
// Focus management
|
||||
async focus(): Promise<void>;
|
||||
async blur(): Promise<void>;
|
||||
|
||||
// Cleanup
|
||||
async cleanup(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Type Widgets
|
||||
|
||||
#### TextTypeWidget
|
||||
|
||||
```typescript
|
||||
class TextTypeWidget extends TypeWidget {
|
||||
static getType() { return 'text'; }
|
||||
|
||||
private textEditor: TextEditor;
|
||||
|
||||
async doRender() {
|
||||
const $editor = $('<div class="ck-editor">');
|
||||
this.textEditor = await TextEditor.create($editor[0], {
|
||||
noteId: this.noteId,
|
||||
content: await this.note.getContent()
|
||||
});
|
||||
|
||||
return $editor;
|
||||
}
|
||||
|
||||
async getContent() {
|
||||
return this.textEditor.getData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CodeTypeWidget
|
||||
|
||||
```typescript
|
||||
class CodeTypeWidget extends TypeWidget {
|
||||
static getType() { return 'code'; }
|
||||
|
||||
private codeMirror: CodeMirror;
|
||||
|
||||
async doRender() {
|
||||
const $container = $('<div class="code-editor">');
|
||||
|
||||
this.codeMirror = CodeMirror($container[0], {
|
||||
value: await this.note.getContent(),
|
||||
mode: this.note.mime,
|
||||
theme: 'default',
|
||||
lineNumbers: true
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Composition
|
||||
|
||||
### Container Widgets
|
||||
|
||||
```typescript
|
||||
// Flexible container layouts
|
||||
class FlexContainer extends BasicWidget {
|
||||
constructor(private direction: 'row' | 'column') {
|
||||
super();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $('<div class="flex-container">')
|
||||
.css('display', 'flex')
|
||||
.css('flex-direction', this.direction);
|
||||
|
||||
for (const child of this.children) {
|
||||
this.$widget.append(child.render());
|
||||
}
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab container
|
||||
class TabContainer extends BasicWidget {
|
||||
private tabs: Array<{title: string, widget: BasicWidget}> = [];
|
||||
|
||||
addTab(title: string, widget: BasicWidget) {
|
||||
this.tabs.push({title, widget});
|
||||
this.child(widget);
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
// Render tab headers and content panels
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composite Widgets
|
||||
|
||||
```typescript
|
||||
class NoteEditorWidget extends NoteContextAwareWidget {
|
||||
private typeWidget: TypeWidget;
|
||||
private titleWidget: NoteTitleWidget;
|
||||
private toolbarWidget: NoteToolbarWidget;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.child(
|
||||
this.toolbarWidget = new NoteToolbarWidget(),
|
||||
this.titleWidget = new NoteTitleWidget(),
|
||||
// Type widget added dynamically
|
||||
);
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Remove old type widget
|
||||
if (this.typeWidget) {
|
||||
this.typeWidget.remove();
|
||||
}
|
||||
|
||||
// Add appropriate type widget
|
||||
const WidgetClass = typeWidgetService.getWidgetClass(note.type);
|
||||
this.typeWidget = new WidgetClass();
|
||||
this.child(this.typeWidget);
|
||||
|
||||
await this.typeWidget.refresh();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Communication
|
||||
|
||||
### Event System
|
||||
|
||||
```typescript
|
||||
// Publishing events
|
||||
class PublisherWidget extends BasicWidget {
|
||||
async handleClick() {
|
||||
// Local event
|
||||
this.trigger('itemSelected', { itemId: '123' });
|
||||
|
||||
// Global event
|
||||
appContext.triggerEvent('noteChanged', { noteId: this.noteId });
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribing to events
|
||||
class SubscriberWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Local event subscription
|
||||
this.on('itemSelected', (event) => {
|
||||
console.log('Item selected:', event.itemId);
|
||||
});
|
||||
|
||||
// Global event subscription
|
||||
appContext.addEventListener('noteChanged', (event) => {
|
||||
this.handleNoteChange(event.noteId);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command System
|
||||
|
||||
```typescript
|
||||
// Registering commands
|
||||
class CommandWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.bindCommand('saveNote', () => this.saveNote());
|
||||
this.bindCommand('deleteNote', () => this.deleteNote());
|
||||
}
|
||||
|
||||
getCommands() {
|
||||
return [
|
||||
{
|
||||
command: 'myWidget:doAction',
|
||||
handler: () => this.doAction(),
|
||||
hotkey: 'ctrl+shift+a'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Widget Development
|
||||
|
||||
### Creating Custom Widgets
|
||||
|
||||
```typescript
|
||||
// 1. Define widget class
|
||||
class TaskListWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="task-list-widget">');
|
||||
this.$list = $('<ul>').appendTo(this.$widget);
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
const tasks = await this.loadTasks(note);
|
||||
|
||||
this.$list.empty();
|
||||
for (const task of tasks) {
|
||||
$('<li>')
|
||||
.text(task.title)
|
||||
.toggleClass('completed', task.completed)
|
||||
.appendTo(this.$list);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTasks(note: FNote) {
|
||||
// Load task data from note attributes
|
||||
const taskLabels = note.getLabels('task');
|
||||
return taskLabels.map(label => JSON.parse(label.value));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register widget
|
||||
api.addWidget(TaskListWidget);
|
||||
```
|
||||
|
||||
### Widget Lifecycle
|
||||
|
||||
```typescript
|
||||
class LifecycleWidget extends NoteContextAwareWidget {
|
||||
// 1. Construction
|
||||
constructor() {
|
||||
super();
|
||||
console.log('Widget constructed');
|
||||
}
|
||||
|
||||
// 2. Initial render
|
||||
doRender() {
|
||||
console.log('Initial render');
|
||||
return $('<div>');
|
||||
}
|
||||
|
||||
// 3. Context initialization
|
||||
async refresh() {
|
||||
console.log('Context refresh');
|
||||
await super.refresh();
|
||||
}
|
||||
|
||||
// 4. Note updates
|
||||
async refreshWithNote(note: FNote) {
|
||||
console.log('Note refresh:', note.noteId);
|
||||
}
|
||||
|
||||
// 5. Cleanup
|
||||
async cleanup() {
|
||||
console.log('Widget cleanup');
|
||||
// Release resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
class LazyWidget extends BasicWidget {
|
||||
private contentLoaded = false;
|
||||
|
||||
async becomeVisible() {
|
||||
if (!this.contentLoaded) {
|
||||
await this.loadContent();
|
||||
this.contentLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadContent() {
|
||||
// Heavy content loading
|
||||
const data = await server.get('expensive-data');
|
||||
this.renderContent(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debouncing Updates
|
||||
|
||||
```typescript
|
||||
class DebouncedWidget extends NoteContextAwareWidget {
|
||||
private refreshDebounced = utils.debounce(
|
||||
() => this.doRefresh(),
|
||||
500
|
||||
);
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Debounce rapid updates
|
||||
this.refreshDebounced();
|
||||
}
|
||||
|
||||
private async doRefresh() {
|
||||
// Actual refresh logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
```typescript
|
||||
class VirtualListWidget extends BasicWidget {
|
||||
private visibleItems: any[] = [];
|
||||
|
||||
renderVisibleItems(scrollTop: number) {
|
||||
const itemHeight = 30;
|
||||
const containerHeight = this.$widget.height();
|
||||
|
||||
const startIndex = Math.floor(scrollTop / itemHeight);
|
||||
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
|
||||
|
||||
this.visibleItems = this.allItems.slice(startIndex, endIndex);
|
||||
this.renderItems();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Widget Design
|
||||
|
||||
1. **Single Responsibility**: Each widget should have one clear purpose
|
||||
2. **Composition over Inheritance**: Use composition for complex UIs
|
||||
3. **Lazy Initialization**: Load resources only when needed
|
||||
4. **Event Cleanup**: Remove event listeners in cleanup()
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
class StatefulWidget extends NoteContextAwareWidget {
|
||||
private state = {
|
||||
isExpanded: false,
|
||||
selectedItems: new Set<string>()
|
||||
};
|
||||
|
||||
setState(updates: Partial<typeof this.state>) {
|
||||
Object.assign(this.state, updates);
|
||||
this.renderState();
|
||||
}
|
||||
|
||||
private renderState() {
|
||||
this.$widget.toggleClass('expanded', this.state.isExpanded);
|
||||
// Update DOM based on state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
class ResilientWidget extends BasicWidget {
|
||||
async refreshWithNote(note: FNote) {
|
||||
try {
|
||||
await this.loadData(note);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load data');
|
||||
console.error('Widget error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
this.$widget.html(`
|
||||
<div class="alert alert-danger">
|
||||
${message}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Widgets
|
||||
|
||||
```typescript
|
||||
// Widget test example
|
||||
describe('TaskListWidget', () => {
|
||||
let widget: TaskListWidget;
|
||||
let note: FNote;
|
||||
|
||||
beforeEach(() => {
|
||||
widget = new TaskListWidget();
|
||||
note = createMockNote({
|
||||
noteId: 'test123',
|
||||
attributes: [
|
||||
{ type: 'label', name: 'task', value: '{"title":"Task 1"}' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should render tasks', async () => {
|
||||
await widget.refreshWithNote(note);
|
||||
|
||||
const tasks = widget.$widget.find('li');
|
||||
expect(tasks.length).toBe(1);
|
||||
expect(tasks.text()).toBe('Task 1');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [Frontend Basics](#root/8Cc2LnZFcF3K) - Frontend scripting guide
|
||||
* [Custom Widgets](#root/VAerafq2qHO1) - Creating custom widgets
|
||||
* [Script API](#root/7Pp4moCrBVzA) - Widget API reference
|
||||
1422
docs/Developer Guide/Developer Guide/Plugin Development/Backend Script Development.md
vendored
Normal file
1422
docs/Developer Guide/Developer Guide/Plugin Development/Backend Script Development.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1624
docs/Developer Guide/Developer Guide/Plugin Development/Custom Note Type Development.md
vendored
Normal file
1624
docs/Developer Guide/Developer Guide/Plugin Development/Custom Note Type Development.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
694
docs/Developer Guide/Developer Guide/Plugin Development/Custom Widget Development Guid.md
vendored
Normal file
694
docs/Developer Guide/Developer Guide/Plugin Development/Custom Widget Development Guid.md
vendored
Normal file
@@ -0,0 +1,694 @@
|
||||
# Custom Widget Development Guide
|
||||
Widgets are the building blocks of Trilium's user interface. This guide shows you how to create your own widgets to extend Trilium with custom functionality.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To develop widgets, you'll need basic JavaScript knowledge and familiarity with jQuery. Widgets in Trilium follow a simple hierarchy where each type adds specific capabilities - BasicWidget for general UI, NoteContextAwareWidget for note-responsive widgets, and RightPanelWidget for sidebar panels.
|
||||
|
||||
## Creating Your First Widget
|
||||
|
||||
### Basic Widget
|
||||
|
||||
Start with a simple widget that displays static content:
|
||||
|
||||
```javascript
|
||||
class MyWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>Hello from my widget!</div>');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Note-Aware Widget
|
||||
|
||||
To make your widget respond to note changes, extend NoteContextAwareWidget:
|
||||
|
||||
```javascript
|
||||
class NoteInfoWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="note-info"></div>');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$widget.html(`
|
||||
<h3>${note.title}</h3>
|
||||
<p>Type: ${note.type}</p>
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `refreshWithNote` method is automatically called whenever the user switches to a different note.
|
||||
|
||||
### Right Panel Widget
|
||||
|
||||
For widgets in the sidebar, extend RightPanelWidget:
|
||||
|
||||
```javascript
|
||||
class StatsWidget extends RightPanelWidget {
|
||||
get widgetTitle() { return "Statistics"; }
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html('<div class="stats">Loading...</div>');
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const content = await note.getContent();
|
||||
const words = content.split(/\s+/).length;
|
||||
this.$body.find('.stats').text(`Words: ${words}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Lifecycle
|
||||
|
||||
Widgets go through three main phases:
|
||||
|
||||
**Initialization**: The `doRender()` method creates your widget's HTML structure. This happens once when the widget is first displayed.
|
||||
|
||||
**Updates**: The `refresh()` or `refreshWithNote()` methods update your widget's content. These are called when data changes or the user switches notes.
|
||||
|
||||
**Cleanup**: If your widget creates timers or external connections, override `cleanup()` to properly dispose of them.
|
||||
|
||||
## Handling Events
|
||||
|
||||
Widgets automatically subscribe to events based on method names. Simply define a method ending with "Event" to handle that event:
|
||||
|
||||
```javascript
|
||||
class ReactiveWidget extends NoteContextAwareWidget {
|
||||
// Triggered when note content changes
|
||||
async noteContentChangedEvent({ noteId }) {
|
||||
if (this.noteId === noteId) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Triggered when user switches notes
|
||||
async noteSwitchedEvent() {
|
||||
console.log('Switched to:', this.noteId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Common events include `noteSwitched`, `noteContentChanged`, and `entitiesReloaded`. The event system ensures your widget stays synchronized with Trilium's state.
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State
|
||||
|
||||
Store widget-specific state in instance properties:
|
||||
|
||||
```typescript
|
||||
class StatefulWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.isExpanded = false;
|
||||
this.cachedData = null;
|
||||
}
|
||||
|
||||
toggleExpanded() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.$widget.toggleClass('expanded', this.isExpanded);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Persistent State
|
||||
|
||||
Use options or attributes for persistent state:
|
||||
|
||||
```typescript
|
||||
class PersistentWidget extends NoteContextAwareWidget {
|
||||
async saveState(state) {
|
||||
await server.put('options', {
|
||||
name: 'widgetState',
|
||||
value: JSON.stringify(state)
|
||||
});
|
||||
}
|
||||
|
||||
async loadState() {
|
||||
const option = await server.get('options/widgetState');
|
||||
return option ? JSON.parse(option.value) : {};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Accessing Trilium APIs
|
||||
|
||||
### Frontend Services
|
||||
|
||||
```typescript
|
||||
import froca from "../services/froca.js";
|
||||
import server from "../services/server.js";
|
||||
import linkService from "../services/link.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
|
||||
class ApiWidget extends NoteContextAwareWidget {
|
||||
async doRenderBody() {
|
||||
// Access notes
|
||||
const note = await froca.getNote(this.noteId);
|
||||
|
||||
// Get attributes
|
||||
const attributes = note.getAttributes();
|
||||
|
||||
// Create links
|
||||
const $link = await linkService.createLink(note.noteId);
|
||||
|
||||
// Show notifications
|
||||
toastService.showMessage("Widget loaded");
|
||||
|
||||
// Open dialogs
|
||||
const result = await dialogService.confirm("Continue?");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Server Communication
|
||||
|
||||
```typescript
|
||||
class ServerWidget extends BasicWidget {
|
||||
async loadData() {
|
||||
// GET request
|
||||
const data = await server.get('custom-api/data');
|
||||
|
||||
// POST request
|
||||
const result = await server.post('custom-api/process', {
|
||||
noteId: this.noteId,
|
||||
action: 'analyze'
|
||||
});
|
||||
|
||||
// PUT request
|
||||
await server.put(`notes/${this.noteId}`, {
|
||||
title: 'Updated Title'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Styling Widgets
|
||||
|
||||
### Inline Styles
|
||||
|
||||
```typescript
|
||||
class StyledWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
this.css('padding', '10px')
|
||||
.css('background-color', '#f0f0f0')
|
||||
.css('border-radius', '4px');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Classes
|
||||
|
||||
```typescript
|
||||
class ClassedWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
this.class('custom-widget')
|
||||
.class('bordered');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Blocks
|
||||
|
||||
```typescript
|
||||
class CSSBlockWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="my-widget">Content</div>');
|
||||
|
||||
this.cssBlock(`
|
||||
.my-widget {
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.my-widget:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
class LazyWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataLoaded = false;
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
if (!this.isVisible()) {
|
||||
return; // Don't load if not visible
|
||||
}
|
||||
|
||||
if (!this.dataLoaded) {
|
||||
await this.loadExpensiveData();
|
||||
this.dataLoaded = true;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debouncing Updates
|
||||
|
||||
```typescript
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
|
||||
class DebouncedWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.performUpdate();
|
||||
}, 500); // 500ms delay
|
||||
}
|
||||
|
||||
async handleInput(value) {
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
```typescript
|
||||
class CachedWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
async getProcessedData(noteId) {
|
||||
if (!this.cache.has(noteId)) {
|
||||
const data = await this.processExpensiveOperation(noteId);
|
||||
this.cache.set(noteId, data);
|
||||
}
|
||||
return this.cache.get(noteId);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Widgets
|
||||
|
||||
### Console Logging
|
||||
|
||||
```typescript
|
||||
class DebugWidget extends BasicWidget {
|
||||
doRender() {
|
||||
console.log('Widget rendering', this.componentId);
|
||||
console.time('render');
|
||||
|
||||
this.$widget = $('<div>');
|
||||
|
||||
console.timeEnd('render');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
class SafeWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
try {
|
||||
await this.riskyOperation();
|
||||
} catch (error) {
|
||||
console.error('Widget error:', error);
|
||||
this.logRenderingError(error);
|
||||
this.$widget.html('<div class="error">Failed to load</div>');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Development Tools
|
||||
|
||||
```typescript
|
||||
class DevWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
|
||||
// Add debug information in development
|
||||
if (window.glob.isDev) {
|
||||
this.$widget.attr('data-debug', 'true');
|
||||
this.$widget.append(`
|
||||
<div class="debug-info">
|
||||
Component ID: ${this.componentId}
|
||||
Position: ${this.position}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example: Note Statistics Widget
|
||||
|
||||
Here's a complete example implementing a custom note statistics widget:
|
||||
|
||||
```typescript
|
||||
import RightPanelWidget from "../widgets/right_panel_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import froca from "../services/froca.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
|
||||
class NoteStatisticsWidget extends RightPanelWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Initialize state
|
||||
this.statistics = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: 0,
|
||||
links: 0,
|
||||
images: 0
|
||||
};
|
||||
|
||||
// Debounce updates for performance
|
||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.calculateStatistics();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
get widgetTitle() {
|
||||
return "Note Statistics";
|
||||
}
|
||||
|
||||
get help() {
|
||||
return {
|
||||
title: "Note Statistics",
|
||||
text: "Displays various statistics about the current note including word count, reading time, and more."
|
||||
};
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html(`
|
||||
<div class="note-statistics">
|
||||
<div class="stat-group">
|
||||
<h5>Content</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Words:</span>
|
||||
<span class="stat-value" data-stat="words">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Characters:</span>
|
||||
<span class="stat-value" data-stat="characters">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Paragraphs:</span>
|
||||
<span class="stat-value" data-stat="paragraphs">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group">
|
||||
<h5>Reading</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Reading time:</span>
|
||||
<span class="stat-value" data-stat="readingTime">0 min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group">
|
||||
<h5>Elements</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Links:</span>
|
||||
<span class="stat-value" data-stat="links">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Images:</span>
|
||||
<span class="stat-value" data-stat="images">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-actions">
|
||||
<button class="btn btn-sm refresh-stats">Refresh</button>
|
||||
<button class="btn btn-sm export-stats">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
this.cssBlock(`
|
||||
.note-statistics {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.stat-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-group h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
`);
|
||||
|
||||
// Bind events
|
||||
this.$body.on('click', '.refresh-stats', () => this.handleRefresh());
|
||||
this.$body.on('click', '.export-stats', () => this.handleExport());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
if (!note) {
|
||||
this.clearStatistics();
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule statistics calculation
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
async calculateStatistics() {
|
||||
try {
|
||||
const note = this.note;
|
||||
if (!note) return;
|
||||
|
||||
const content = await note.getContent();
|
||||
|
||||
if (note.type === 'text') {
|
||||
// Parse HTML content
|
||||
const $content = $('<div>').html(content);
|
||||
const textContent = $content.text();
|
||||
|
||||
// Calculate statistics
|
||||
this.statistics.words = this.countWords(textContent);
|
||||
this.statistics.characters = textContent.length;
|
||||
this.statistics.paragraphs = $content.find('p').length;
|
||||
this.statistics.readingTime = Math.ceil(this.statistics.words / 200);
|
||||
this.statistics.links = $content.find('a').length;
|
||||
this.statistics.images = $content.find('img').length;
|
||||
} else if (note.type === 'code') {
|
||||
// For code notes, count lines and characters
|
||||
const lines = content.split('\n');
|
||||
this.statistics.words = lines.length; // Show lines instead of words
|
||||
this.statistics.characters = content.length;
|
||||
this.statistics.paragraphs = 0;
|
||||
this.statistics.readingTime = 0;
|
||||
this.statistics.links = 0;
|
||||
this.statistics.images = 0;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to calculate statistics:', error);
|
||||
toastService.showError("Failed to calculate statistics");
|
||||
}
|
||||
}
|
||||
|
||||
countWords(text) {
|
||||
const words = text.match(/\b\w+\b/g);
|
||||
return words ? words.length : 0;
|
||||
}
|
||||
|
||||
clearStatistics() {
|
||||
this.statistics = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: 0,
|
||||
links: 0,
|
||||
images: 0
|
||||
};
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
this.$body.find('[data-stat="words"]').text(this.statistics.words);
|
||||
this.$body.find('[data-stat="characters"]').text(this.statistics.characters);
|
||||
this.$body.find('[data-stat="paragraphs"]').text(this.statistics.paragraphs);
|
||||
this.$body.find('[data-stat="readingTime"]').text(`${this.statistics.readingTime} min`);
|
||||
this.$body.find('[data-stat="links"]').text(this.statistics.links);
|
||||
this.$body.find('[data-stat="images"]').text(this.statistics.images);
|
||||
}
|
||||
|
||||
async handleRefresh() {
|
||||
await this.calculateStatistics();
|
||||
toastService.showMessage("Statistics refreshed");
|
||||
}
|
||||
|
||||
async handleExport() {
|
||||
const note = this.note;
|
||||
if (!note) return;
|
||||
|
||||
const exportData = {
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
statistics: this.statistics,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Create a CSV
|
||||
const csv = [
|
||||
'Metric,Value',
|
||||
`Words,${this.statistics.words}`,
|
||||
`Characters,${this.statistics.characters}`,
|
||||
`Paragraphs,${this.statistics.paragraphs}`,
|
||||
`Reading Time,${this.statistics.readingTime} minutes`,
|
||||
`Links,${this.statistics.links}`,
|
||||
`Images,${this.statistics.images}`
|
||||
].join('\n');
|
||||
|
||||
// Download CSV
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `statistics-${note.noteId}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toastService.showMessage("Statistics exported");
|
||||
}
|
||||
|
||||
async noteContentChangedEvent({ noteId }) {
|
||||
if (this.noteId === noteId) {
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.$body.off('click');
|
||||
this.spacedUpdate = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteStatisticsWidget;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1\. Memory Management
|
||||
|
||||
* Clean up event listeners in `cleanup()`
|
||||
* Clear caches and timers when widget is destroyed
|
||||
* Avoid circular references
|
||||
|
||||
### 2\. Performance
|
||||
|
||||
* Use debouncing for frequent updates
|
||||
* Implement lazy loading for expensive operations
|
||||
* Cache computed values when appropriate
|
||||
|
||||
### 3\. Error Handling
|
||||
|
||||
* Always wrap async operations in try-catch
|
||||
* Provide user feedback for errors
|
||||
* Log errors for debugging
|
||||
|
||||
### 4\. User Experience
|
||||
|
||||
* Show loading states for async operations
|
||||
* Provide clear error messages
|
||||
* Ensure widgets are responsive
|
||||
|
||||
### 5\. Code Organization
|
||||
|
||||
* Keep widgets focused on a single responsibility
|
||||
* Extract reusable logic into services
|
||||
* Use composition over inheritance when possible
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Widget Not Rendering
|
||||
|
||||
* Check `doRender()` creates `this.$widget`
|
||||
* Verify widget is properly registered
|
||||
* Check console for errors
|
||||
|
||||
### Events Not Firing
|
||||
|
||||
* Ensure event method name matches pattern: `${eventName}Event`
|
||||
* Check event is being triggered
|
||||
* Verify widget is active/visible
|
||||
|
||||
### State Not Persisting
|
||||
|
||||
* Use options or attributes for persistence
|
||||
* Check save operations complete successfully
|
||||
* Verify data serialization
|
||||
|
||||
### Performance Issues
|
||||
|
||||
* Profile with browser dev tools
|
||||
* Implement caching and debouncing
|
||||
* Optimize DOM operations
|
||||
|
||||
## Next Steps
|
||||
|
||||
* Explore existing widgets in `/apps/client/src/widgets/` for examples
|
||||
* Review the Frontend Script API documentation
|
||||
* Join the Trilium community for support and sharing widgets
|
||||
1042
docs/Developer Guide/Developer Guide/Plugin Development/Frontend Script Development.md
vendored
Normal file
1042
docs/Developer Guide/Developer Guide/Plugin Development/Frontend Script Development.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1173
docs/Developer Guide/Developer Guide/Plugin Development/Theme Development Guide.md
vendored
Normal file
1173
docs/Developer Guide/Developer Guide/Plugin Development/Theme Development Guide.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
docs/Release Notes/!!!meta.json
vendored
2
docs/Release Notes/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.97.2",
|
||||
"appVersion": "0.98.0",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
2
docs/Release Notes/Release Notes/v0.98.0.md
vendored
2
docs/Release Notes/Release Notes/v0.98.0.md
vendored
@@ -44,12 +44,14 @@
|
||||
## 🌍 Internationalization
|
||||
|
||||
* Improvements to multiple languages:
|
||||
|
||||
* Chinese (Traditional)
|
||||
* Spanish
|
||||
* Some work started on new languages:
|
||||
|
||||
Portuguese (Brazil), Japanese, Russian, Serbian, Italian, Greek, Catalan
|
||||
* Added new languages:
|
||||
|
||||
* Russian (translations by @questamor)
|
||||
* Japanese language (translations by [acwr47](https://hosted.weblate.org/user/acwr47/))\[…\]
|
||||
|
||||
|
||||
855
docs/User Guide/!!!meta.json
vendored
855
docs/User Guide/!!!meta.json
vendored
File diff suppressed because it is too large
Load Diff
465
docs/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.md
vendored
Normal file
465
docs/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.md
vendored
Normal file
@@ -0,0 +1,465 @@
|
||||
# Advanced-Search-Expressions
|
||||
## Advanced Search Expressions
|
||||
|
||||
This guide covers complex search expressions that combine multiple criteria, use advanced operators, and leverage Trilium's relationship system for sophisticated queries.
|
||||
|
||||
## Complex Query Construction
|
||||
|
||||
### Boolean Logic with Parentheses
|
||||
|
||||
Use parentheses to group expressions and control evaluation order:
|
||||
|
||||
```
|
||||
(#book OR #article) AND #author=Tolkien
|
||||
```
|
||||
|
||||
Finds notes that are either books or articles, written by Tolkien.
|
||||
|
||||
```
|
||||
#project AND (#status=active OR #status=pending)
|
||||
```
|
||||
|
||||
Finds active or pending projects.
|
||||
|
||||
```
|
||||
meeting AND (#priority=high OR #urgent) AND note.dateCreated >= TODAY-7
|
||||
```
|
||||
|
||||
Finds recent high-priority or urgent meetings.
|
||||
|
||||
### Negation Patterns
|
||||
|
||||
Use `NOT` or the `not()` function to exclude certain criteria:
|
||||
|
||||
```
|
||||
#book AND not(#genre=fiction)
|
||||
```
|
||||
|
||||
Finds non-fiction books.
|
||||
|
||||
```
|
||||
project AND not(note.isArchived=true)
|
||||
```
|
||||
|
||||
Finds non-archived notes containing "project".
|
||||
|
||||
```
|
||||
#!completed
|
||||
```
|
||||
|
||||
Short syntax for notes without the "completed" label.
|
||||
|
||||
### Mixed Search Types
|
||||
|
||||
Combine full-text, attribute, and property searches:
|
||||
|
||||
```
|
||||
development #category=work note.type=text note.dateModified >= TODAY-30
|
||||
```
|
||||
|
||||
Finds text notes about development, categorized as work, modified in the last 30 days.
|
||||
|
||||
## Advanced Attribute Searches
|
||||
|
||||
### Fuzzy Attribute Matching
|
||||
|
||||
When fuzzy attribute search is enabled, you can use partial matches:
|
||||
|
||||
```
|
||||
#lang
|
||||
```
|
||||
|
||||
Matches labels like "language", "languages", "programming-lang", etc.
|
||||
|
||||
```
|
||||
#category=prog
|
||||
```
|
||||
|
||||
Matches categories like "programming", "progress", "program", etc.
|
||||
|
||||
### Multiple Attribute Conditions
|
||||
|
||||
```
|
||||
#book #author=Tolkien #publicationYear>=1950 #publicationYear<1960
|
||||
```
|
||||
|
||||
Finds Tolkien's books published in the 1950s.
|
||||
|
||||
```
|
||||
#task #priority=high #status!=completed
|
||||
```
|
||||
|
||||
Finds high-priority incomplete tasks.
|
||||
|
||||
### Complex Label Value Patterns
|
||||
|
||||
Use various operators for sophisticated label matching:
|
||||
|
||||
```
|
||||
#isbn %= '978-[0-9-]+'
|
||||
```
|
||||
|
||||
Finds notes with ISBN labels matching the pattern (regex).
|
||||
|
||||
```
|
||||
#email *=* @company.com
|
||||
```
|
||||
|
||||
Finds notes with email labels containing "@company.com".
|
||||
|
||||
```
|
||||
#version >= 2.0
|
||||
```
|
||||
|
||||
Finds notes with version labels of 2.0 or higher (numeric comparison).
|
||||
|
||||
## Relationship Traversal
|
||||
|
||||
### Basic Relation Queries
|
||||
|
||||
```
|
||||
~author.title *=* Tolkien
|
||||
```
|
||||
|
||||
Finds notes with an "author" relation to notes containing "Tolkien" in the title.
|
||||
|
||||
```
|
||||
~project.labels.status = active
|
||||
```
|
||||
|
||||
Finds notes related to projects with active status.
|
||||
|
||||
### Multi-Level Relationships
|
||||
|
||||
```
|
||||
~author.relations.publisher.title = "Penguin Books"
|
||||
```
|
||||
|
||||
Finds notes authored by someone published by Penguin Books.
|
||||
|
||||
```
|
||||
~project.children.title *=* documentation
|
||||
```
|
||||
|
||||
Finds notes related to projects that have child notes about documentation.
|
||||
|
||||
### Relationship Direction
|
||||
|
||||
```
|
||||
note.children.title = "Chapter 1"
|
||||
```
|
||||
|
||||
Finds parent notes that have a child titled "Chapter 1".
|
||||
|
||||
```
|
||||
note.parents.labels.category = book
|
||||
```
|
||||
|
||||
Finds notes whose parents are categorized as books.
|
||||
|
||||
```
|
||||
note.ancestors.title = "Literature"
|
||||
```
|
||||
|
||||
Finds notes with "Literature" anywhere in their ancestor chain.
|
||||
|
||||
## Property-Based Searches
|
||||
|
||||
### Note Metadata Queries
|
||||
|
||||
```
|
||||
note.type=code note.mime=text/javascript note.dateCreated >= MONTH
|
||||
```
|
||||
|
||||
Finds JavaScript code notes created this month.
|
||||
|
||||
```
|
||||
note.isProtected=true note.contentSize > 1000
|
||||
```
|
||||
|
||||
Finds large protected notes.
|
||||
|
||||
```
|
||||
note.childrenCount >= 10 note.type=text
|
||||
```
|
||||
|
||||
Finds text notes with many children.
|
||||
|
||||
### Advanced Property Combinations
|
||||
|
||||
```
|
||||
note.parentCount > 1 #template
|
||||
```
|
||||
|
||||
Finds template notes that are cloned in multiple places.
|
||||
|
||||
```
|
||||
note.attributeCount > 5 note.type=text note.contentSize < 500
|
||||
```
|
||||
|
||||
Finds small text notes with many attributes (heavily tagged short notes).
|
||||
|
||||
```
|
||||
note.revisionCount > 10 note.dateModified >= TODAY-7
|
||||
```
|
||||
|
||||
Finds frequently edited notes modified recently.
|
||||
|
||||
## Date and Time Expressions
|
||||
|
||||
### Relative Date Calculations
|
||||
|
||||
```
|
||||
#dueDate <= TODAY+7 #dueDate >= TODAY
|
||||
```
|
||||
|
||||
Finds tasks due in the next week.
|
||||
|
||||
```
|
||||
note.dateCreated >= MONTH-2 note.dateCreated < MONTH
|
||||
```
|
||||
|
||||
Finds notes created in the past two months.
|
||||
|
||||
```
|
||||
#eventDate = YEAR note.dateCreated >= YEAR-1
|
||||
```
|
||||
|
||||
Finds events scheduled for this year that were planned last year.
|
||||
|
||||
### Complex Date Logic
|
||||
|
||||
```
|
||||
(#startDate <= TODAY AND #endDate >= TODAY) OR #status=ongoing
|
||||
```
|
||||
|
||||
Finds current events or ongoing items.
|
||||
|
||||
```
|
||||
#reminderDate <= NOW+3600 #reminderDate > NOW
|
||||
```
|
||||
|
||||
Finds reminders due in the next hour (using seconds offset).
|
||||
|
||||
## Fuzzy Search Techniques
|
||||
|
||||
### Fuzzy Exact Matching
|
||||
|
||||
```
|
||||
#title ~= managment
|
||||
```
|
||||
|
||||
Finds notes with titles like "management" even with typos.
|
||||
|
||||
```
|
||||
~category.title ~= progaming
|
||||
```
|
||||
|
||||
Finds notes related to categories like "programming" with misspellings.
|
||||
|
||||
### Fuzzy Contains Matching
|
||||
|
||||
```
|
||||
note.content ~* algoritm
|
||||
```
|
||||
|
||||
Finds notes containing words like "algorithm" with spelling variations.
|
||||
|
||||
```
|
||||
#description ~* recieve
|
||||
```
|
||||
|
||||
Finds notes with descriptions containing "receive" despite the common misspelling.
|
||||
|
||||
### Progressive Fuzzy Strategy
|
||||
|
||||
By default, Trilium uses exact matching first, then fuzzy as fallback:
|
||||
|
||||
```
|
||||
development project
|
||||
```
|
||||
|
||||
First finds exact matches for "development" and "project", then adds fuzzy matches if needed.
|
||||
|
||||
To force fuzzy behavior:
|
||||
|
||||
```
|
||||
#title ~= development #category ~= projet
|
||||
```
|
||||
|
||||
## Ordering and Limiting
|
||||
|
||||
### Multiple Sort Criteria
|
||||
|
||||
```
|
||||
#book orderBy #publicationYear desc, note.title asc limit 20
|
||||
```
|
||||
|
||||
Orders books by publication year (newest first), then by title alphabetically, limited to 20 results.
|
||||
|
||||
```
|
||||
#task orderBy #priority desc, #dueDate asc
|
||||
```
|
||||
|
||||
Orders tasks by priority (high first), then by due date (earliest first).
|
||||
|
||||
### Dynamic Ordering
|
||||
|
||||
```
|
||||
#meeting note.dateCreated >= TODAY-30 orderBy note.dateModified desc
|
||||
```
|
||||
|
||||
Finds recent meetings ordered by last modification.
|
||||
|
||||
```
|
||||
#project #status=active orderBy note.childrenCount desc limit 10
|
||||
```
|
||||
|
||||
Finds the 10 most complex active projects (by number of sub-notes).
|
||||
|
||||
## Performance Optimization Patterns
|
||||
|
||||
### Efficient Query Structure
|
||||
|
||||
Start with the most selective criteria:
|
||||
|
||||
```
|
||||
#book #author=Tolkien note.dateCreated >= 1950-01-01
|
||||
```
|
||||
|
||||
Better than:
|
||||
|
||||
```
|
||||
note.dateCreated >= 1950-01-01 #book #author=Tolkien
|
||||
```
|
||||
|
||||
### Fast Search for Large Datasets
|
||||
|
||||
```
|
||||
#category=project #status=active
|
||||
```
|
||||
|
||||
With fast search enabled, this searches only attributes, not content.
|
||||
|
||||
### Limiting Expensive Operations
|
||||
|
||||
```
|
||||
note.content *=* "complex search term" limit 50
|
||||
```
|
||||
|
||||
Limits content search to prevent performance issues.
|
||||
|
||||
## Error Handling and Debugging
|
||||
|
||||
### Syntax Validation
|
||||
|
||||
Invalid syntax produces helpful error messages:
|
||||
|
||||
```
|
||||
#book AND OR #author=Tolkien
|
||||
```
|
||||
|
||||
Error: "Mixed usage of AND/OR - always use parentheses to group AND/OR expressions."
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug mode to see how queries are parsed:
|
||||
|
||||
```
|
||||
#book #author=Tolkien
|
||||
```
|
||||
|
||||
With debug enabled, shows the internal expression tree structure.
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
* Unescaped special characters: Use quotes or backslashes
|
||||
* Missing parentheses in complex boolean expressions
|
||||
* Incorrect property names: Use `note.title` not `title`
|
||||
* Case sensitivity assumptions: All searches are case-insensitive
|
||||
|
||||
## Expression Shortcuts
|
||||
|
||||
### Label Shortcuts
|
||||
|
||||
Full syntax:
|
||||
|
||||
```
|
||||
note.labels.category = book
|
||||
```
|
||||
|
||||
Shortcut:
|
||||
|
||||
```
|
||||
#category = book
|
||||
```
|
||||
|
||||
### Relation Shortcuts
|
||||
|
||||
Full syntax:
|
||||
|
||||
```
|
||||
note.relations.author.title *=* Tolkien
|
||||
```
|
||||
|
||||
Shortcut:
|
||||
|
||||
```
|
||||
~author.title *=* Tolkien
|
||||
```
|
||||
|
||||
### Property Shortcuts
|
||||
|
||||
Some properties have convenient shortcuts:
|
||||
|
||||
```
|
||||
note.text *=* content
|
||||
```
|
||||
|
||||
Searches both title and content for "content".
|
||||
|
||||
## Real-World Complex Examples
|
||||
|
||||
### Project Management
|
||||
|
||||
```
|
||||
(#project OR #task) AND #status!=completed AND
|
||||
(#priority=high OR #dueDate <= TODAY+7) AND
|
||||
not(note.isArchived=true)
|
||||
orderBy #priority desc, #dueDate asc
|
||||
```
|
||||
|
||||
### Research Organization
|
||||
|
||||
```
|
||||
(#paper OR #article OR #book) AND
|
||||
~author.title *=* smith AND
|
||||
#topic *=* "machine learning" AND
|
||||
note.dateCreated >= YEAR-2
|
||||
orderBy #citationCount desc limit 25
|
||||
```
|
||||
|
||||
### Content Management
|
||||
|
||||
```
|
||||
note.type=text AND note.contentSize > 5000 AND
|
||||
#category=documentation AND note.childrenCount >= 3 AND
|
||||
note.dateModified >= MONTH-1
|
||||
orderBy note.dateModified desc
|
||||
```
|
||||
|
||||
### Knowledge Base Maintenance
|
||||
|
||||
```
|
||||
note.attributeCount = 0 AND note.childrenCount = 0 AND
|
||||
note.parentCount = 1 AND note.contentSize < 100 AND
|
||||
note.dateModified < TODAY-90
|
||||
```
|
||||
|
||||
Finds potential cleanup candidates: small, untagged, isolated notes not modified in 90 days.
|
||||
|
||||
## Next Steps
|
||||
|
||||
* [Search Examples and Use Cases](Search-Examples-and-Use-Cases.md) - Practical applications
|
||||
* [Saved Searches](Saved-Searches.md) - Creating reusable search configurations
|
||||
* [Technical Search Details](Technical-Search-Details.md) - Implementation details and performance tuning
|
||||
160
docs/User Guide/User Guide/Advanced Usage/Search/README.md
vendored
Normal file
160
docs/User Guide/User Guide/Advanced Usage/Search/README.md
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
# README
|
||||
## Trilium Search Documentation
|
||||
|
||||
Welcome to the comprehensive guide for Trilium's powerful search capabilities. This documentation covers everything from basic text searches to advanced query expressions and performance optimization.
|
||||
|
||||
## Quick Start
|
||||
|
||||
New to Trilium search? Start here:
|
||||
|
||||
* **[Search Fundamentals](Search-Fundamentals.md)** - Basic concepts, syntax, and operators
|
||||
|
||||
## Documentation Sections
|
||||
|
||||
### Core Search Features
|
||||
|
||||
* **[Search Fundamentals](Search-Fundamentals.md)** - Basic search syntax, operators, and concepts
|
||||
* **[Advanced Search Expressions](Advanced-Search-Expressions.md)** - Complex queries, boolean logic, and relationship traversal
|
||||
|
||||
### Practical Applications
|
||||
|
||||
* **[Search Examples and Use Cases](Search-Examples-and-Use-Cases.md)** - Real-world examples for common workflows
|
||||
* **[Saved Searches](Saved-Searches.md)** - Creating dynamic collections and dashboards
|
||||
|
||||
### Technical Reference
|
||||
|
||||
* **[Technical Search Details](Technical-Search-Details.md)** - Performance, implementation, and optimization
|
||||
|
||||
## Key Search Capabilities
|
||||
|
||||
### Full-Text Search
|
||||
|
||||
* Search note titles and content
|
||||
* Exact phrase matching with quotes
|
||||
* Case-insensitive with diacritic normalization
|
||||
* Support for multiple note types (text, code, mermaid, canvas)
|
||||
|
||||
### Attribute-Based Search
|
||||
|
||||
* Label searches: `#tag`, `#category=book`
|
||||
* Relation searches: `~author`, `~author.title=Tolkien`
|
||||
* Complex attribute combinations
|
||||
* Fuzzy attribute matching
|
||||
|
||||
### Property Search
|
||||
|
||||
* Note metadata: `note.type=text`, `note.dateCreated >= TODAY-7`
|
||||
* Hierarchical queries: `note.parents.title=Books`
|
||||
* Relationship traversal: `note.children.labels.status=active`
|
||||
|
||||
### Advanced Features
|
||||
|
||||
* **Progressive Search**: Exact matching first, fuzzy fallback when needed
|
||||
* **Fuzzy Search**: Typo tolerance and spelling variations
|
||||
* **Boolean Logic**: Complex AND/OR/NOT combinations
|
||||
* **Date Arithmetic**: Dynamic date calculations (TODAY-30, YEAR+1)
|
||||
* **Regular Expressions**: Pattern matching with `%=` operator
|
||||
* **Ordering and Limiting**: Custom sort orders and result limits
|
||||
|
||||
## Search Operators Quick Reference
|
||||
|
||||
### Text Operators
|
||||
|
||||
* `=` - Exact match
|
||||
* `!=` - Not equal
|
||||
* `*=*` - Contains
|
||||
* `=*` - Starts with
|
||||
* `*=` - Ends with
|
||||
* `%=` - Regular expression
|
||||
* `~=` - Fuzzy exact match
|
||||
* `~*` - Fuzzy contains match
|
||||
|
||||
### Numeric Operators
|
||||
|
||||
* `=`, `!=`, `>`, `>=`, `<`, `<=`
|
||||
|
||||
### Boolean Operators
|
||||
|
||||
* `AND`, `OR`, `NOT`
|
||||
|
||||
### Special Syntax
|
||||
|
||||
* `#labelName` - Label exists
|
||||
* `#labelName=value` - Label equals value
|
||||
* `~relationName` - Relation exists
|
||||
* `~relationName.property` - Relation target property
|
||||
* `note.property` - Note property access
|
||||
* `"exact phrase"` - Quoted phrase search
|
||||
|
||||
## Common Search Patterns
|
||||
|
||||
### Simple Searches
|
||||
|
||||
```
|
||||
hello world # Find notes containing both words
|
||||
"project management" # Find exact phrase
|
||||
#task # Find notes with "task" label
|
||||
~author # Find notes with "author" relation
|
||||
```
|
||||
|
||||
### Attribute Searches
|
||||
|
||||
```
|
||||
#book #author=Tolkien # Books by Tolkien
|
||||
#task #priority=high #status!=completed # High-priority incomplete tasks
|
||||
~project.title *=* alpha # Notes related to projects with "alpha" in title
|
||||
```
|
||||
|
||||
### Date-Based Searches
|
||||
|
||||
```
|
||||
note.dateCreated >= TODAY-7 # Notes created in last week
|
||||
#dueDate <= TODAY+30 # Items due in next 30 days
|
||||
#eventDate = YEAR # Events scheduled for this year
|
||||
```
|
||||
|
||||
### Complex Queries
|
||||
|
||||
```
|
||||
(#book OR #article) AND #topic=programming AND note.dateModified >= MONTH
|
||||
#project AND (#status=active OR #status=pending) AND not(note.isArchived=true)
|
||||
```
|
||||
|
||||
## Getting Started Checklist
|
||||
|
||||
1. **Learn Basic Syntax** - Start with simple text and tag searches
|
||||
2. **Understand Operators** - Master the core operators (`=`, `*=*`, etc.)
|
||||
3. **Practice Attributes** - Use `#` for labels and `~` for relations
|
||||
4. **Try Boolean Logic** - Combine searches with AND/OR/NOT
|
||||
5. **Explore Properties** - Use `note.` prefix for metadata searches
|
||||
6. **Create Saved Searches** - Turn useful queries into dynamic collections
|
||||
7. **Optimize Performance** - Learn about fast search and limits
|
||||
|
||||
## Performance Tips
|
||||
|
||||
* **Use Fast Search** for attribute-only queries
|
||||
* **Set Reasonable Limits** to prevent large result sets
|
||||
* **Start Specific** with the most selective criteria first
|
||||
* **Leverage Attributes** instead of content search when possible
|
||||
* **Cache Common Queries** as saved searches
|
||||
|
||||
## Need Help?
|
||||
|
||||
* **Examples**: Check [Search Examples and Use Cases](Search-Examples-and-Use-Cases.md) for practical patterns
|
||||
* **Complex Queries**: See [Advanced Search Expressions](Advanced-Search-Expressions.md) for sophisticated techniques
|
||||
* **Performance Issues**: Review [Technical Search Details](Technical-Search-Details.md) for optimization
|
||||
* **Dynamic Collections**: Learn about [Saved Searches](Saved-Searches.md) for automated organization
|
||||
|
||||
## Search Workflow Integration
|
||||
|
||||
Trilium's search integrates seamlessly with your note-taking workflow:
|
||||
|
||||
* **Quick Search** (Ctrl+S) for instant access
|
||||
* **Saved Searches** for dynamic organization
|
||||
* **Search from Subtree** for focused queries
|
||||
* **Auto-complete** suggestions in search dialogs
|
||||
* **URL-triggered searches** for bookmarkable queries
|
||||
|
||||
Start with the fundamentals and gradually explore advanced features as your needs grow. Trilium's search system is designed to scale from simple text queries to sophisticated knowledge management systems.
|
||||
|
||||
Happy searching! 🔍
|
||||
429
docs/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.md
vendored
Normal file
429
docs/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.md
vendored
Normal file
@@ -0,0 +1,429 @@
|
||||
# Saved-Searches
|
||||
## Saved Searches
|
||||
|
||||
Saved searches in Trilium allow you to create dynamic collections of notes that automatically update based on search criteria. They appear as special notes in your tree and provide a powerful way to organize and access related content.
|
||||
|
||||
## Understanding Saved Searches
|
||||
|
||||
A saved search is a special note type that:
|
||||
|
||||
* Stores search criteria and configuration
|
||||
* Dynamically displays matching notes as children
|
||||
* Updates automatically when notes change
|
||||
* Can be bookmarked and accessed like any other note
|
||||
* Supports all search features including ordering and limits
|
||||
|
||||
## Creating Saved Searches
|
||||
|
||||
### From Search Dialog
|
||||
|
||||
1. Open the search dialog (Ctrl+S or search icon)
|
||||
2. Configure your search criteria and options
|
||||
3. Click "Save to note" button
|
||||
4. Choose a name and location for the saved search
|
||||
|
||||
### Manual Creation
|
||||
|
||||
1. Create a new note and set its type to "Saved Search"
|
||||
2. Configure the search using labels:
|
||||
* `#searchString` - The search query
|
||||
* `#fastSearch` - Enable fast search mode
|
||||
* `#includeArchivedNotes` - Include archived notes
|
||||
* `#orderBy` - Sort field
|
||||
* `#orderDirection` - "asc" or "desc"
|
||||
* `#limit` - Maximum number of results
|
||||
|
||||
### Using Search Scripts
|
||||
|
||||
For complex logic, create a JavaScript note and link it:
|
||||
|
||||
* `~searchScript` - Relation pointing to a backend script note
|
||||
|
||||
## Basic Saved Search Examples
|
||||
|
||||
### Simple Text Search
|
||||
|
||||
```
|
||||
#searchString=project management
|
||||
```
|
||||
|
||||
Finds all notes containing "project management".
|
||||
|
||||
### Tag-Based Collection
|
||||
|
||||
```
|
||||
#searchString=#book #author=Tolkien
|
||||
#orderBy=publicationYear
|
||||
#orderDirection=desc
|
||||
```
|
||||
|
||||
Creates a collection of Tolkien's books ordered by publication year.
|
||||
|
||||
### Task Dashboard
|
||||
|
||||
```
|
||||
#searchString=#task #status!=completed #assignee=me
|
||||
#orderBy=priority
|
||||
#orderDirection=desc
|
||||
#limit=20
|
||||
```
|
||||
|
||||
Shows your top 20 incomplete tasks by priority.
|
||||
|
||||
### Recent Activity
|
||||
|
||||
```
|
||||
#searchString=note.dateModified >= TODAY-7
|
||||
#orderBy=dateModified
|
||||
#orderDirection=desc
|
||||
#limit=50
|
||||
```
|
||||
|
||||
Shows the 50 most recently modified notes from the last week.
|
||||
|
||||
## Advanced Saved Search Patterns
|
||||
|
||||
### Dynamic Date-Based Collections
|
||||
|
||||
#### This Week's Content
|
||||
|
||||
```
|
||||
#searchString=note.dateCreated >= TODAY-7 note.dateCreated < TODAY
|
||||
#orderBy=dateCreated
|
||||
#orderDirection=desc
|
||||
```
|
||||
|
||||
#### Monthly Review Collection
|
||||
|
||||
```
|
||||
#searchString=#reviewed=false note.dateCreated >= MONTH note.dateCreated < MONTH+1
|
||||
#orderBy=dateCreated
|
||||
```
|
||||
|
||||
#### Upcoming Deadlines
|
||||
|
||||
```
|
||||
#searchString=#dueDate >= TODAY #dueDate <= TODAY+14 #status!=completed
|
||||
#orderBy=dueDate
|
||||
#orderDirection=asc
|
||||
```
|
||||
|
||||
### Project-Specific Collections
|
||||
|
||||
#### Project Dashboard
|
||||
|
||||
```
|
||||
#searchString=#project=alpha (#task OR #milestone OR #document)
|
||||
#orderBy=priority
|
||||
#orderDirection=desc
|
||||
```
|
||||
|
||||
#### Project Health Monitor
|
||||
|
||||
```
|
||||
#searchString=#project=alpha #status=blocked OR (#dueDate < TODAY #status!=completed)
|
||||
#orderBy=dueDate
|
||||
#orderDirection=asc
|
||||
```
|
||||
|
||||
### Content Type Collections
|
||||
|
||||
#### Documentation Hub
|
||||
|
||||
```
|
||||
#searchString=(#documentation OR #guide OR #manual) #product=api
|
||||
#orderBy=dateModified
|
||||
#orderDirection=desc
|
||||
```
|
||||
|
||||
#### Learning Path
|
||||
|
||||
```
|
||||
#searchString=#course #level=beginner #topic=programming
|
||||
#orderBy=difficulty
|
||||
#orderDirection=asc
|
||||
```
|
||||
|
||||
## Search Script Examples
|
||||
|
||||
For complex logic that can't be expressed in search strings, use JavaScript:
|
||||
|
||||
### Custom Business Logic
|
||||
|
||||
```javascript
|
||||
// Find notes that need attention based on complex criteria
|
||||
const api = require('api');
|
||||
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 30);
|
||||
|
||||
const results = [];
|
||||
|
||||
// Find high-priority tasks overdue by more than a week
|
||||
const overdueTasks = api.searchForNotes(`
|
||||
#task #priority=high #dueDate < TODAY-7 #status!=completed
|
||||
`);
|
||||
|
||||
// Find projects with no recent activity
|
||||
const staleProjets = api.searchForNotes(`
|
||||
#project #status=active note.dateModified < TODAY-30
|
||||
`);
|
||||
|
||||
// Find notes with many attributes but no content
|
||||
const overlabeledNotes = api.searchForNotes(`
|
||||
note.attributeCount > 5 note.contentSize < 100
|
||||
`);
|
||||
|
||||
return [...overdueTasks, ...staleProjects, ...overlabeledNotes]
|
||||
.map(note => note.noteId);
|
||||
```
|
||||
|
||||
### Dynamic Tag-Based Grouping
|
||||
|
||||
```javascript
|
||||
// Group notes by quarter based on creation date
|
||||
const api = require('api');
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const results = [];
|
||||
|
||||
for (let quarter = 1; quarter <= 4; quarter++) {
|
||||
const startMonth = (quarter - 1) * 3 + 1;
|
||||
const endMonth = quarter * 3;
|
||||
|
||||
const quarterNotes = api.searchForNotes(`
|
||||
note.dateCreated >= "${currentYear}-${String(startMonth).padStart(2, '0')}-01"
|
||||
note.dateCreated < "${currentYear}-${String(endMonth + 1).padStart(2, '0')}-01"
|
||||
#project
|
||||
`);
|
||||
|
||||
results.push(...quarterNotes.map(note => note.noteId));
|
||||
}
|
||||
|
||||
return results;
|
||||
```
|
||||
|
||||
### Conditional Search Logic
|
||||
|
||||
```javascript
|
||||
// Smart dashboard that changes based on day of week
|
||||
const api = require('api');
|
||||
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||
|
||||
let searchQuery;
|
||||
|
||||
if (dayOfWeek === 1) { // Monday - weekly planning
|
||||
searchQuery = '#task #status=planned #week=' + getWeekNumber(today);
|
||||
} else if (dayOfWeek === 5) { // Friday - weekly review
|
||||
searchQuery = '#task #completed=true #week=' + getWeekNumber(today);
|
||||
} else { // Regular days - focus on today's work
|
||||
searchQuery = '#task #dueDate=TODAY #status!=completed';
|
||||
}
|
||||
|
||||
const notes = api.searchForNotes(searchQuery);
|
||||
return notes.map(note => note.noteId);
|
||||
|
||||
function getWeekNumber(date) {
|
||||
const firstDay = new Date(date.getFullYear(), 0, 1);
|
||||
const pastDays = Math.floor((date - firstDay) / 86400000);
|
||||
return Math.ceil((pastDays + firstDay.getDay() + 1) / 7);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Fast Search for Large Collections
|
||||
|
||||
For collections that don't need content search:
|
||||
|
||||
```
|
||||
#searchString=#category=reference #type=article
|
||||
#fastSearch=true
|
||||
#limit=100
|
||||
```
|
||||
|
||||
### Efficient Ordering
|
||||
|
||||
Use indexed properties for better performance:
|
||||
|
||||
```
|
||||
#orderBy=dateCreated
|
||||
#orderBy=title
|
||||
#orderBy=noteId
|
||||
```
|
||||
|
||||
Avoid complex calculated orderings in large collections.
|
||||
|
||||
### Result Limiting
|
||||
|
||||
Always set reasonable limits for large collections:
|
||||
|
||||
```
|
||||
#limit=50
|
||||
```
|
||||
|
||||
For very large result sets, consider breaking into multiple saved searches.
|
||||
|
||||
## Saved Search Organization
|
||||
|
||||
### Hierarchical Organization
|
||||
|
||||
Create a folder structure for saved searches:
|
||||
|
||||
```
|
||||
📁 Searches
|
||||
├── 📁 Projects
|
||||
│ ├── 🔍 Active Projects
|
||||
│ ├── 🔍 Overdue Tasks
|
||||
│ └── 🔍 Project Archive
|
||||
├── 📁 Content
|
||||
│ ├── 🔍 Recent Drafts
|
||||
│ ├── 🔍 Published Articles
|
||||
│ └── 🔍 Review Queue
|
||||
└── 📁 Maintenance
|
||||
├── 🔍 Untagged Notes
|
||||
├── 🔍 Cleanup Candidates
|
||||
└── 🔍 Orphaned Notes
|
||||
```
|
||||
|
||||
### Search Naming Conventions
|
||||
|
||||
Use clear, descriptive names:
|
||||
|
||||
* "Active High-Priority Tasks"
|
||||
* "This Month's Meeting Notes"
|
||||
* "Unprocessed Inbox Items"
|
||||
* "Literature Review Papers"
|
||||
|
||||
### Search Labels
|
||||
|
||||
Tag saved searches for organization:
|
||||
|
||||
```
|
||||
#searchType=dashboard
|
||||
#searchType=maintenance
|
||||
#searchType=archive
|
||||
#frequency=daily
|
||||
#frequency=weekly
|
||||
```
|
||||
|
||||
## Dashboard Creation
|
||||
|
||||
### Personal Dashboard
|
||||
|
||||
Combine multiple saved searches in a parent note:
|
||||
|
||||
```
|
||||
📋 My Dashboard
|
||||
├── 🔍 Today's Tasks
|
||||
├── 🔍 Urgent Items
|
||||
├── 🔍 Recent Notes
|
||||
├── 🔍 Upcoming Deadlines
|
||||
└── 🔍 Weekly Review Items
|
||||
```
|
||||
|
||||
### Project Dashboard
|
||||
|
||||
```
|
||||
📋 Project Alpha Dashboard
|
||||
├── 🔍 Active Tasks
|
||||
├── 🔍 Blocked Items
|
||||
├── 🔍 Recent Updates
|
||||
├── 🔍 Milestones
|
||||
└── 🔍 Team Notes
|
||||
```
|
||||
|
||||
### Content Dashboard
|
||||
|
||||
```
|
||||
📋 Content Management
|
||||
├── 🔍 Draft Articles
|
||||
├── 🔍 Review Queue
|
||||
├── 🔍 Published This Month
|
||||
├── 🔍 High-Engagement Posts
|
||||
└── 🔍 Content Ideas
|
||||
```
|
||||
|
||||
## Maintenance and Updates
|
||||
|
||||
### Regular Review
|
||||
|
||||
Periodically review saved searches for:
|
||||
|
||||
* Outdated search criteria
|
||||
* Performance issues
|
||||
* Unused collections
|
||||
* Scope creep
|
||||
|
||||
### Search Evolution
|
||||
|
||||
As your note-taking evolves, update searches:
|
||||
|
||||
* Add new tags to existing searches
|
||||
* Refine criteria based on usage patterns
|
||||
* Split large collections into smaller ones
|
||||
* Merge rarely-used collections
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
Watch for performance issues:
|
||||
|
||||
* Slow-loading saved searches
|
||||
* Memory usage with large result sets
|
||||
* Search timeout errors
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Empty Results
|
||||
|
||||
* Check search syntax
|
||||
* Verify tag spellings
|
||||
* Ensure notes have required attributes
|
||||
* Test search components individually
|
||||
|
||||
#### Performance Problems
|
||||
|
||||
* Add `#fastSearch=true` for attribute-only searches
|
||||
* Reduce result limits
|
||||
* Simplify complex criteria
|
||||
* Use indexed properties for ordering
|
||||
|
||||
#### Unexpected Results
|
||||
|
||||
* Enable debug mode to see query parsing
|
||||
* Test search in search dialog first
|
||||
* Check for case sensitivity issues
|
||||
* Verify date formats and ranges
|
||||
|
||||
### Best Practices
|
||||
|
||||
#### Search Design
|
||||
|
||||
* Start simple and add complexity gradually
|
||||
* Test searches thoroughly before saving
|
||||
* Document complex search logic
|
||||
* Use meaningful names and descriptions
|
||||
|
||||
#### Performance
|
||||
|
||||
* Set appropriate limits
|
||||
* Use fast search when possible
|
||||
* Avoid overly complex expressions
|
||||
* Monitor search execution time
|
||||
|
||||
#### Organization
|
||||
|
||||
* Group related searches
|
||||
* Use consistent naming conventions
|
||||
* Archive unused searches
|
||||
* Regular cleanup and maintenance
|
||||
|
||||
## Next Steps
|
||||
|
||||
* [Technical Search Details](Technical-Search-Details.md) - Understanding search performance and implementation
|
||||
* [Search Examples and Use Cases](Search-Examples-and-Use-Cases.md) - More practical examples
|
||||
* [Advanced Search Expressions](Advanced-Search-Expressions.md) - Complex query construction
|
||||
539
docs/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.md
vendored
Normal file
539
docs/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.md
vendored
Normal file
@@ -0,0 +1,539 @@
|
||||
# Search-Examples-and-Use-Cases
|
||||
## Search Examples and Use Cases
|
||||
|
||||
This guide provides practical examples of how to use Trilium's search capabilities for common organizational patterns and workflows.
|
||||
|
||||
## Personal Knowledge Management
|
||||
|
||||
### Research and Learning
|
||||
|
||||
Track your learning progress and find related materials:
|
||||
|
||||
```
|
||||
#topic=javascript #status=learning
|
||||
```
|
||||
|
||||
Find all JavaScript materials you're currently learning.
|
||||
|
||||
```
|
||||
#course #completed=false note.dateCreated >= MONTH-1
|
||||
```
|
||||
|
||||
Find courses started in the last month that aren't completed.
|
||||
|
||||
```
|
||||
#book #topic *=* programming #rating >= 4
|
||||
```
|
||||
|
||||
Find highly-rated programming books.
|
||||
|
||||
```
|
||||
#paper ~author.title *=* "Andrew Ng" #field=machine-learning
|
||||
```
|
||||
|
||||
Find machine learning papers by Andrew Ng.
|
||||
|
||||
### Meeting and Event Management
|
||||
|
||||
Organize meetings, notes, and follow-ups:
|
||||
|
||||
```
|
||||
#meeting note.dateCreated >= TODAY-7 #attendee *=* smith
|
||||
```
|
||||
|
||||
Find this week's meetings with Smith.
|
||||
|
||||
```
|
||||
#meeting #actionItems #status!=completed
|
||||
```
|
||||
|
||||
Find meetings with outstanding action items.
|
||||
|
||||
```
|
||||
#event #date >= TODAY #date <= TODAY+30
|
||||
```
|
||||
|
||||
Find upcoming events in the next 30 days.
|
||||
|
||||
```
|
||||
#meeting #project=alpha note.dateCreated >= MONTH
|
||||
```
|
||||
|
||||
Find this month's meetings about project alpha.
|
||||
|
||||
### Note Organization and Cleanup
|
||||
|
||||
Maintain and organize your note structure:
|
||||
|
||||
```
|
||||
note.childrenCount = 0 note.parentCount = 1 note.contentSize < 50 note.dateModified < TODAY-180
|
||||
```
|
||||
|
||||
Find small, isolated notes not modified in 6 months (cleanup candidates).
|
||||
|
||||
```
|
||||
note.attributeCount = 0 note.type=text note.contentSize > 1000
|
||||
```
|
||||
|
||||
Find large text notes without any labels (might need categorization).
|
||||
|
||||
```
|
||||
#draft note.dateCreated < TODAY-30
|
||||
```
|
||||
|
||||
Find old draft notes that might need attention.
|
||||
|
||||
```
|
||||
note.parentCount > 3 note.type=text
|
||||
```
|
||||
|
||||
Find notes that are heavily cloned (might indicate important content).
|
||||
|
||||
## Project Management
|
||||
|
||||
### Task Tracking
|
||||
|
||||
Manage tasks and project progress:
|
||||
|
||||
```
|
||||
#task #priority=high #status!=completed #assignee=me
|
||||
```
|
||||
|
||||
Find your high-priority incomplete tasks.
|
||||
|
||||
```
|
||||
#task #dueDate <= TODAY+3 #dueDate >= TODAY #status!=completed
|
||||
```
|
||||
|
||||
Find tasks due in the next 3 days.
|
||||
|
||||
```
|
||||
#project=website #task #status=blocked
|
||||
```
|
||||
|
||||
Find blocked tasks in the website project.
|
||||
|
||||
```
|
||||
#task #estimatedHours > 0 #actualHours > 0 orderBy note.dateModified desc
|
||||
```
|
||||
|
||||
Find tasks with time tracking data, sorted by recent updates.
|
||||
|
||||
### Project Oversight
|
||||
|
||||
Monitor project health and progress:
|
||||
|
||||
```
|
||||
#project #status=active note.children.labels.status = blocked
|
||||
```
|
||||
|
||||
Find active projects with blocked tasks.
|
||||
|
||||
```
|
||||
#project #startDate <= TODAY-90 #status!=completed
|
||||
```
|
||||
|
||||
Find projects that started over 90 days ago but aren't completed.
|
||||
|
||||
```
|
||||
#milestone #targetDate <= TODAY #status!=achieved
|
||||
```
|
||||
|
||||
Find overdue milestones.
|
||||
|
||||
```
|
||||
#project orderBy note.childrenCount desc limit 10
|
||||
```
|
||||
|
||||
Find the 10 largest projects by number of sub-notes.
|
||||
|
||||
### Resource Planning
|
||||
|
||||
Track resources and dependencies:
|
||||
|
||||
```
|
||||
#resource #type=person #availability < 50
|
||||
```
|
||||
|
||||
Find people with low availability.
|
||||
|
||||
```
|
||||
#dependency #status=pending #project=mobile-app
|
||||
```
|
||||
|
||||
Find pending dependencies for the mobile app project.
|
||||
|
||||
```
|
||||
#budget #project #spent > #allocated
|
||||
```
|
||||
|
||||
Find projects over budget.
|
||||
|
||||
## Content Creation and Writing
|
||||
|
||||
### Writing Projects
|
||||
|
||||
Manage articles, books, and documentation:
|
||||
|
||||
```
|
||||
#article #status=draft #wordCount >= 1000
|
||||
```
|
||||
|
||||
Find substantial draft articles.
|
||||
|
||||
```
|
||||
#chapter #book=novel #status=outline
|
||||
```
|
||||
|
||||
Find novel chapters still in outline stage.
|
||||
|
||||
```
|
||||
#blog-post #published=false #topic=technology
|
||||
```
|
||||
|
||||
Find unpublished technology blog posts.
|
||||
|
||||
```
|
||||
#documentation #lastReviewed < TODAY-90 #product=api
|
||||
```
|
||||
|
||||
Find API documentation not reviewed in 90 days.
|
||||
|
||||
### Editorial Workflow
|
||||
|
||||
Track editing and publication status:
|
||||
|
||||
```
|
||||
#article #editor=jane #status=review
|
||||
```
|
||||
|
||||
Find articles assigned to Jane for review.
|
||||
|
||||
```
|
||||
#manuscript #submissionDate >= TODAY-30 #status=pending
|
||||
```
|
||||
|
||||
Find manuscripts submitted in the last 30 days still pending.
|
||||
|
||||
```
|
||||
#publication #acceptanceDate >= YEAR #status=accepted
|
||||
```
|
||||
|
||||
Find accepted publications this year.
|
||||
|
||||
### Content Research
|
||||
|
||||
Organize research materials and sources:
|
||||
|
||||
```
|
||||
#source #reliability >= 8 #topic *=* climate
|
||||
```
|
||||
|
||||
Find reliable sources about climate topics.
|
||||
|
||||
```
|
||||
#quote #author *=* Einstein #verified=true
|
||||
```
|
||||
|
||||
Find verified Einstein quotes.
|
||||
|
||||
```
|
||||
#citation #used=false #relevance=high
|
||||
```
|
||||
|
||||
Find high-relevance citations not yet used.
|
||||
|
||||
## Business and Professional Use
|
||||
|
||||
### Client Management
|
||||
|
||||
Track client relationships and projects:
|
||||
|
||||
```
|
||||
#client=acme #project #status=active
|
||||
```
|
||||
|
||||
Find active projects for ACME client.
|
||||
|
||||
```
|
||||
#meeting #client #date >= MONTH #followUp=required
|
||||
```
|
||||
|
||||
Find client meetings this month requiring follow-up.
|
||||
|
||||
```
|
||||
#contract #renewalDate <= TODAY+60 #renewalDate >= TODAY
|
||||
```
|
||||
|
||||
Find contracts expiring in the next 60 days.
|
||||
|
||||
```
|
||||
#invoice #status=unpaid #dueDate < TODAY
|
||||
```
|
||||
|
||||
Find overdue unpaid invoices.
|
||||
|
||||
### Process Documentation
|
||||
|
||||
Maintain procedures and workflows:
|
||||
|
||||
```
|
||||
#procedure #department=engineering #lastUpdated < TODAY-365
|
||||
```
|
||||
|
||||
Find engineering procedures not updated in a year.
|
||||
|
||||
```
|
||||
#workflow #status=active #automation=possible
|
||||
```
|
||||
|
||||
Find active workflows that could be automated.
|
||||
|
||||
```
|
||||
#checklist #process=onboarding #role=developer
|
||||
```
|
||||
|
||||
Find onboarding checklists for developers.
|
||||
|
||||
### Compliance and Auditing
|
||||
|
||||
Track compliance requirements and audits:
|
||||
|
||||
```
|
||||
#compliance #standard=sox #nextReview <= TODAY+30
|
||||
```
|
||||
|
||||
Find SOX compliance items due for review soon.
|
||||
|
||||
```
|
||||
#audit #finding #severity=high #status!=resolved
|
||||
```
|
||||
|
||||
Find unresolved high-severity audit findings.
|
||||
|
||||
```
|
||||
#policy #department=hr #effectiveDate >= YEAR
|
||||
```
|
||||
|
||||
Find HR policies that became effective this year.
|
||||
|
||||
## Academic and Educational Use
|
||||
|
||||
### Course Management
|
||||
|
||||
Organize courses and educational content:
|
||||
|
||||
```
|
||||
#course #semester=fall-2024 #assignment #dueDate >= TODAY
|
||||
```
|
||||
|
||||
Find upcoming assignments for fall 2024 courses.
|
||||
|
||||
```
|
||||
#lecture #course=physics #topic *=* quantum
|
||||
```
|
||||
|
||||
Find physics lectures about quantum topics.
|
||||
|
||||
```
|
||||
#student #grade < 70 #course=mathematics
|
||||
```
|
||||
|
||||
Find students struggling in mathematics.
|
||||
|
||||
```
|
||||
#syllabus #course #lastUpdated < TODAY-180
|
||||
```
|
||||
|
||||
Find syllabi not updated in 6 months.
|
||||
|
||||
### Research Management
|
||||
|
||||
Track research projects and publications:
|
||||
|
||||
```
|
||||
#experiment #status=running #endDate <= TODAY+7
|
||||
```
|
||||
|
||||
Find experiments ending in the next week.
|
||||
|
||||
```
|
||||
#dataset #size > 1000000 #cleaned=true #public=false
|
||||
```
|
||||
|
||||
Find large, cleaned, private datasets.
|
||||
|
||||
```
|
||||
#hypothesis #tested=false #priority=high
|
||||
```
|
||||
|
||||
Find high-priority untested hypotheses.
|
||||
|
||||
```
|
||||
#collaboration #institution *=* stanford #status=active
|
||||
```
|
||||
|
||||
Find active collaborations with Stanford.
|
||||
|
||||
### Grant and Funding
|
||||
|
||||
Manage funding applications and requirements:
|
||||
|
||||
```
|
||||
#grant #deadline <= TODAY+30 #deadline >= TODAY #status=in-progress
|
||||
```
|
||||
|
||||
Find grant applications due in the next 30 days.
|
||||
|
||||
```
|
||||
#funding #amount >= 100000 #status=awarded #startDate >= YEAR
|
||||
```
|
||||
|
||||
Find large grants awarded this year.
|
||||
|
||||
```
|
||||
#report #funding #dueDate <= TODAY+14 #status!=submitted
|
||||
```
|
||||
|
||||
Find funding reports due in 2 weeks.
|
||||
|
||||
## Technical Documentation
|
||||
|
||||
### Code and Development
|
||||
|
||||
Track code-related notes and documentation:
|
||||
|
||||
```
|
||||
#bug #severity=critical #status!=fixed #product=webapp
|
||||
```
|
||||
|
||||
Find critical unfixed bugs in the web app.
|
||||
|
||||
```
|
||||
#feature #version=2.0 #status=implemented #tested=false
|
||||
```
|
||||
|
||||
Find version 2.0 features that are implemented but not tested.
|
||||
|
||||
```
|
||||
#api #endpoint #deprecated=true #removalDate <= TODAY+90
|
||||
```
|
||||
|
||||
Find deprecated API endpoints scheduled for removal soon.
|
||||
|
||||
```
|
||||
#architecture #component=database #lastReviewed < TODAY-180
|
||||
```
|
||||
|
||||
Find database architecture documentation not reviewed in 6 months.
|
||||
|
||||
### System Administration
|
||||
|
||||
Manage infrastructure and operations:
|
||||
|
||||
```
|
||||
#server #status=maintenance #scheduledDate >= TODAY #scheduledDate <= TODAY+7
|
||||
```
|
||||
|
||||
Find servers scheduled for maintenance this week.
|
||||
|
||||
```
|
||||
#backup #status=failed #date >= TODAY-7
|
||||
```
|
||||
|
||||
Find backup failures in the last week.
|
||||
|
||||
```
|
||||
#security #vulnerability #severity=high #patched=false
|
||||
```
|
||||
|
||||
Find unpatched high-severity vulnerabilities.
|
||||
|
||||
```
|
||||
#monitoring #alert #frequency > 10 #period=week
|
||||
```
|
||||
|
||||
Find alerts triggering more than 10 times per week.
|
||||
|
||||
## Data Analysis and Reporting
|
||||
|
||||
### Performance Tracking
|
||||
|
||||
Monitor metrics and KPIs:
|
||||
|
||||
```
|
||||
#metric #kpi=true #trend=declining #period=month
|
||||
```
|
||||
|
||||
Find declining monthly KPIs.
|
||||
|
||||
```
|
||||
#report #frequency=weekly #lastGenerated < TODAY-10
|
||||
```
|
||||
|
||||
Find weekly reports that haven't been generated in 10 days.
|
||||
|
||||
```
|
||||
#dashboard #stakeholder=executive #lastUpdated < TODAY-7
|
||||
```
|
||||
|
||||
Find executive dashboards not updated this week.
|
||||
|
||||
### Trend Analysis
|
||||
|
||||
Track patterns and changes over time:
|
||||
|
||||
```
|
||||
#data #source=sales #period=quarter #analyzed=false
|
||||
```
|
||||
|
||||
Find unanalyzed quarterly sales data.
|
||||
|
||||
```
|
||||
#trend #direction=up #significance=high #period=month
|
||||
```
|
||||
|
||||
Find significant positive monthly trends.
|
||||
|
||||
```
|
||||
#forecast #accuracy < 80 #model=linear #period=quarter
|
||||
```
|
||||
|
||||
Find inaccurate quarterly linear forecasts.
|
||||
|
||||
## Search Strategy Tips
|
||||
|
||||
### Building Effective Queries
|
||||
|
||||
1. **Start Specific**: Begin with the most selective criteria
|
||||
2. **Add Gradually**: Build complexity incrementally
|
||||
3. **Test Components**: Verify each part of complex queries
|
||||
4. **Use Shortcuts**: Leverage `#` and `~` shortcuts for efficiency
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
1. **Use Fast Search**: For large databases, enable fast search when content isn't needed
|
||||
2. **Limit Results**: Add limits to prevent overwhelming result sets
|
||||
3. **Order Strategically**: Put the most useful results first
|
||||
4. **Cache Common Queries**: Save frequently used searches
|
||||
|
||||
### Maintenance Patterns
|
||||
|
||||
Regular queries for note maintenance:
|
||||
|
||||
```
|
||||
# Weekly cleanup check
|
||||
note.attributeCount = 0 note.type=text note.contentSize < 100 note.dateModified < TODAY-30
|
||||
|
||||
# Monthly project review
|
||||
#project #status=active note.dateModified < TODAY-30
|
||||
|
||||
# Quarterly archive review
|
||||
note.isArchived=false note.dateModified < TODAY-90 note.childrenCount = 0
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
* [Saved Searches](Saved-Searches.md) - Convert these examples into reusable saved searches
|
||||
* [Technical Search Details](Technical-Search-Details.md) - Understanding performance and implementation
|
||||
* [Search Fundamentals](Search-Fundamentals.md) - Review basic concepts and syntax
|
||||
214
docs/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.md
vendored
Normal file
214
docs/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.md
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
# Search-Fundamentals
|
||||
## Search Fundamentals
|
||||
|
||||
Trilium's search system is a powerful tool for finding and organizing notes. It supports multiple search modes, from simple text queries to complex expressions using attributes, relationships, and note properties.
|
||||
|
||||
## Search Types Overview
|
||||
|
||||
Trilium provides three main search approaches:
|
||||
|
||||
1. **Full-text Search** - Searches within note titles and content
|
||||
2. **Attribute Search** - Searches based on labels and relations attached to notes
|
||||
3. **Property Search** - Searches based on note metadata (type, creation date, etc.)
|
||||
|
||||
These can be combined in powerful ways to create precise queries.
|
||||
|
||||
## Basic Search Syntax
|
||||
|
||||
### Simple Text Search
|
||||
|
||||
```
|
||||
hello world
|
||||
```
|
||||
|
||||
Finds notes containing both "hello" and "world" anywhere in the title or content.
|
||||
|
||||
### Quoted Text Search
|
||||
|
||||
```
|
||||
"hello world"
|
||||
```
|
||||
|
||||
Finds notes containing the exact phrase "hello world".
|
||||
|
||||
### Attribute Search
|
||||
|
||||
```
|
||||
#tag
|
||||
```
|
||||
|
||||
Finds notes with the label "tag".
|
||||
|
||||
```
|
||||
#category=book
|
||||
```
|
||||
|
||||
Finds notes with label "category" set to "book".
|
||||
|
||||
### Relation Search
|
||||
|
||||
```
|
||||
~author
|
||||
```
|
||||
|
||||
Finds notes with a relation named "author".
|
||||
|
||||
```
|
||||
~author.title=Tolkien
|
||||
```
|
||||
|
||||
Finds notes with an "author" relation pointing to a note titled "Tolkien".
|
||||
|
||||
## Search Operators
|
||||
|
||||
### Text Operators
|
||||
|
||||
* `=` - Exact match
|
||||
* `!=` - Not equal
|
||||
* `*=*` - Contains (substring)
|
||||
* `=*` - Starts with
|
||||
* `*=` - Ends with
|
||||
* `%=` - Regular expression match
|
||||
* `~=` - Fuzzy exact match
|
||||
* `~*` - Fuzzy contains match
|
||||
|
||||
### Numeric Operators
|
||||
|
||||
* `=` - Equal
|
||||
* `!=` - Not equal
|
||||
* `>` - Greater than
|
||||
* `>=` - Greater than or equal
|
||||
* `<` - Less than
|
||||
* `<=` - Less than or equal
|
||||
|
||||
### Boolean Operators
|
||||
|
||||
* `AND` - Both conditions must be true
|
||||
* `OR` - Either condition must be true
|
||||
* `NOT` or `not()` - Condition must be false
|
||||
|
||||
## Search Context and Scope
|
||||
|
||||
### Search Scope
|
||||
|
||||
By default, search covers:
|
||||
|
||||
* Note titles
|
||||
* Note content (for text-based note types)
|
||||
* Label names and values
|
||||
* Relation names
|
||||
* Note properties
|
||||
|
||||
### Fast Search Mode
|
||||
|
||||
When enabled, fast search:
|
||||
|
||||
* Searches only titles and attributes
|
||||
* Skips note content
|
||||
* Provides faster results for large databases
|
||||
|
||||
### Archived Notes
|
||||
|
||||
* Excluded by default
|
||||
* Can be included with "Include archived" option
|
||||
|
||||
## Case Sensitivity and Normalization
|
||||
|
||||
* All searches are case-insensitive
|
||||
* Diacritics are normalized ("café" matches "cafe")
|
||||
* Unicode characters are properly handled
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Content Size Limits
|
||||
|
||||
* Note content is limited to 10MB for search processing
|
||||
* Larger notes are still searchable by title and attributes
|
||||
|
||||
### Progressive Search Strategy
|
||||
|
||||
1. **Exact Search Phase**: Fast exact matching (handles 90%+ of searches)
|
||||
2. **Fuzzy Search Phase**: Activated when exact search returns fewer than 5 high-quality results
|
||||
3. **Result Ordering**: Exact matches always appear before fuzzy matches
|
||||
|
||||
### Search Optimization Tips
|
||||
|
||||
* Use specific terms rather than very common words
|
||||
* Combine full-text with attribute searches for precision
|
||||
* Use fast search for large databases when content search isn't needed
|
||||
* Limit results when dealing with very large result sets
|
||||
|
||||
## Special Characters and Escaping
|
||||
|
||||
### Reserved Characters
|
||||
|
||||
These characters have special meaning in search queries:
|
||||
|
||||
* `#` - Label indicator
|
||||
* `~` - Relation indicator
|
||||
* `()` - Grouping
|
||||
* `"` `'` `` ` `` - Quotes for exact phrases
|
||||
|
||||
### Escaping Special Characters
|
||||
|
||||
Use backslash to search for literal special characters:
|
||||
|
||||
```
|
||||
\#hashtag
|
||||
```
|
||||
|
||||
Searches for the literal text "#hashtag" instead of a label.
|
||||
|
||||
Use quotes to include special characters in phrases:
|
||||
|
||||
```
|
||||
"note.txt file"
|
||||
```
|
||||
|
||||
Searches for the exact phrase including the dot.
|
||||
|
||||
## Date and Time Values
|
||||
|
||||
### Special Date Keywords
|
||||
|
||||
* `TODAY` - Current date
|
||||
* `NOW` - Current date and time
|
||||
* `MONTH` - Current month
|
||||
* `YEAR` - Current year
|
||||
|
||||
### Date Arithmetic
|
||||
|
||||
```
|
||||
#dateCreated >= TODAY-30
|
||||
```
|
||||
|
||||
Finds notes created in the last 30 days.
|
||||
|
||||
```
|
||||
#eventDate = YEAR+1
|
||||
```
|
||||
|
||||
Finds notes with eventDate set to next year.
|
||||
|
||||
## Search Results and Scoring
|
||||
|
||||
### Result Ranking
|
||||
|
||||
Results are ordered by:
|
||||
|
||||
1. Relevance score (based on term frequency and position)
|
||||
2. Note depth (closer to root ranks higher)
|
||||
3. Alphabetical order for ties
|
||||
|
||||
### Progressive Search Behavior
|
||||
|
||||
* Exact matches always rank before fuzzy matches
|
||||
* High-quality exact matches prevent fuzzy search activation
|
||||
* Fuzzy matches help find content with typos or variations
|
||||
|
||||
## Next Steps
|
||||
|
||||
* [Advanced Search Expressions](Advanced-Search-Expressions.md) - Complex queries and combinations
|
||||
* [Search Examples and Use Cases](Search-Examples-and-Use-Cases.md) - Practical applications
|
||||
* [Saved Searches](Saved-Searches.md) - Creating dynamic collections
|
||||
* [Technical Search Details](Technical-Search-Details.md) - Under-the-hood implementation
|
||||
589
docs/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.md
vendored
Normal file
589
docs/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.md
vendored
Normal file
@@ -0,0 +1,589 @@
|
||||
# Technical-Search-Details
|
||||
## Technical Search Details
|
||||
|
||||
This guide provides technical information about Trilium's search implementation, performance characteristics, and optimization strategies for power users and administrators.
|
||||
|
||||
## Search Architecture Overview
|
||||
|
||||
### Three-Layer Search System
|
||||
|
||||
Trilium's search operates across three cache layers:
|
||||
|
||||
1. **Becca (Backend Cache)**: Server-side entity cache containing notes, attributes, and relationships
|
||||
2. **Froca (Frontend Cache)**: Client-side mirror providing fast UI updates
|
||||
3. **Database Layer**: SQLite database with FTS (Full-Text Search) support
|
||||
|
||||
### Search Processing Pipeline
|
||||
|
||||
1. **Lexical Analysis**: Query parsing and tokenization
|
||||
2. **Expression Building**: Converting tokens to executable expressions
|
||||
3. **Progressive Execution**: Exact search followed by optional fuzzy search
|
||||
4. **Result Scoring**: Relevance calculation and ranking
|
||||
5. **Result Presentation**: Formatting and highlighting
|
||||
|
||||
## Query Processing Details
|
||||
|
||||
### Lexical Analysis (Lex)
|
||||
|
||||
The lexer breaks down search queries into components:
|
||||
|
||||
```javascript
|
||||
// Input: 'project #status=active note.dateCreated >= TODAY-7'
|
||||
// Output:
|
||||
{
|
||||
fulltextTokens: ['project'],
|
||||
expressionTokens: ['#status', '=', 'active', 'note', '.', 'dateCreated', '>=', 'TODAY-7']
|
||||
}
|
||||
```
|
||||
|
||||
#### Token Types
|
||||
|
||||
* **Fulltext Tokens**: Regular search terms
|
||||
* **Expression Tokens**: Attributes, operators, and property references
|
||||
* **Quoted Strings**: Exact phrase matches
|
||||
* **Escaped Characters**: Literal special characters
|
||||
|
||||
### Expression Building (Parse)
|
||||
|
||||
Tokens are converted into executable expression trees:
|
||||
|
||||
```javascript
|
||||
// Expression tree for: #book AND #author=Tolkien
|
||||
AndExp([
|
||||
AttributeExistsExp('label', 'book'),
|
||||
LabelComparisonExp('label', 'author', equals('tolkien'))
|
||||
])
|
||||
```
|
||||
|
||||
#### Expression Types
|
||||
|
||||
* `AndExp`, `OrExp`, `NotExp`: Boolean logic
|
||||
* `AttributeExistsExp`: Label/relation existence
|
||||
* `LabelComparisonExp`: Label value comparison
|
||||
* `RelationWhereExp`: Relation target queries
|
||||
* `PropertyComparisonExp`: Note property filtering
|
||||
* `NoteContentFulltextExp`: Content search
|
||||
* `OrderByAndLimitExp`: Result ordering and limiting
|
||||
|
||||
### Progressive Search Strategy
|
||||
|
||||
#### Phase 1: Exact Search
|
||||
|
||||
```javascript
|
||||
// Fast exact matching
|
||||
const exactResults = performSearch(expression, searchContext, false);
|
||||
```
|
||||
|
||||
Characteristics:
|
||||
|
||||
* Substring matching for text
|
||||
* Exact attribute matching
|
||||
* Property-based filtering
|
||||
* Handles 90%+ of searches
|
||||
* Sub-second response time
|
||||
|
||||
#### Phase 2: Fuzzy Fallback
|
||||
|
||||
```javascript
|
||||
// Activated when exact results < 5 high-quality matches
|
||||
if (highQualityResults.length < 5) {
|
||||
const fuzzyResults = performSearch(expression, searchContext, true);
|
||||
return mergeExactAndFuzzyResults(exactResults, fuzzyResults);
|
||||
}
|
||||
```
|
||||
|
||||
Characteristics:
|
||||
|
||||
* Edit distance calculations
|
||||
* Phrase proximity matching
|
||||
* Typo tolerance
|
||||
* Performance safeguards
|
||||
* Exact matches always rank first
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Search Limits and Thresholds
|
||||
|
||||
| Parameter | Value | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `MAX_SEARCH_CONTENT_SIZE` | 2MB | Database-level content filtering |
|
||||
| `MIN_FUZZY_TOKEN_LENGTH` | 3 chars | Minimum length for fuzzy matching |
|
||||
| `MAX_EDIT_DISTANCE` | 2 chars | Maximum character changes for fuzzy |
|
||||
| `MAX_PHRASE_PROXIMITY` | 10 words | Maximum distance for phrase matching |
|
||||
| `RESULT_SUFFICIENCY_THRESHOLD` | 5 results | Threshold for fuzzy activation |
|
||||
| `ABSOLUTE_MAX_CONTENT_SIZE` | 100MB | Hard limit to prevent system crash |
|
||||
| `ABSOLUTE_MAX_WORD_COUNT` | 2M words | Hard limit for word processing |
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
#### Database-Level Optimizations
|
||||
|
||||
```mariadb
|
||||
-- Content size filtering at database level
|
||||
SELECT noteId, type, mime, content, isProtected
|
||||
FROM notes JOIN blobs USING (blobId)
|
||||
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND isDeleted = 0
|
||||
AND LENGTH(content) < 2097152 -- 2MB limit
|
||||
```
|
||||
|
||||
#### Memory Management
|
||||
|
||||
* Single-array edit distance calculation
|
||||
* Early termination for distant matches
|
||||
* Progressive content processing
|
||||
* Cached regular expressions
|
||||
|
||||
#### Search Context Optimization
|
||||
|
||||
```javascript
|
||||
// Efficient search context configuration
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: true, // Skip content search
|
||||
limit: 50, // Reasonable result limit
|
||||
orderBy: 'dateCreated', // Use indexed property
|
||||
includeArchivedNotes: false // Reduce search space
|
||||
});
|
||||
```
|
||||
|
||||
## Fuzzy Search Implementation
|
||||
|
||||
### Edit Distance Algorithm
|
||||
|
||||
Trilium uses an optimized Levenshtein distance calculation:
|
||||
|
||||
```javascript
|
||||
// Optimized single-array implementation
|
||||
function calculateOptimizedEditDistance(str1, str2, maxDistance) {
|
||||
// Early termination checks
|
||||
if (Math.abs(str1.length - str2.length) > maxDistance) {
|
||||
return maxDistance + 1;
|
||||
}
|
||||
|
||||
// Single array optimization
|
||||
let previousRow = Array.from({ length: str2.length + 1 }, (_, i) => i);
|
||||
let currentRow = new Array(str2.length + 1);
|
||||
|
||||
for (let i = 1; i <= str1.length; i++) {
|
||||
currentRow[0] = i;
|
||||
let minInRow = i;
|
||||
|
||||
for (let j = 1; j <= str2.length; j++) {
|
||||
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
||||
currentRow[j] = Math.min(
|
||||
previousRow[j] + 1, // deletion
|
||||
currentRow[j - 1] + 1, // insertion
|
||||
previousRow[j - 1] + cost // substitution
|
||||
);
|
||||
minInRow = Math.min(minInRow, currentRow[j]);
|
||||
}
|
||||
|
||||
// Early termination if row minimum exceeds threshold
|
||||
if (minInRow > maxDistance) return maxDistance + 1;
|
||||
|
||||
[previousRow, currentRow] = [currentRow, previousRow];
|
||||
}
|
||||
|
||||
return previousRow[str2.length];
|
||||
}
|
||||
```
|
||||
|
||||
### Phrase Proximity Matching
|
||||
|
||||
For multi-token fuzzy searches:
|
||||
|
||||
```javascript
|
||||
// Check if tokens appear within reasonable proximity
|
||||
function hasProximityMatch(tokenPositions, maxDistance = 10) {
|
||||
// For 2 tokens, simple distance check
|
||||
if (tokenPositions.length === 2) {
|
||||
const [pos1, pos2] = tokenPositions;
|
||||
return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
|
||||
}
|
||||
|
||||
// For multiple tokens, find sequence within range
|
||||
const findSequence = (remaining, currentPos) => {
|
||||
if (remaining.length === 0) return true;
|
||||
const [nextPositions, ...rest] = remaining;
|
||||
return nextPositions.some(pos =>
|
||||
Math.abs(pos - currentPos) <= maxDistance &&
|
||||
findSequence(rest, pos)
|
||||
);
|
||||
};
|
||||
|
||||
const [firstPositions, ...rest] = tokenPositions;
|
||||
return firstPositions.some(startPos => findSequence(rest, startPos));
|
||||
}
|
||||
```
|
||||
|
||||
## Indexing and Storage
|
||||
|
||||
### Database Schema Optimization
|
||||
|
||||
```mariadb
|
||||
-- Relevant indexes for search performance
|
||||
CREATE INDEX idx_notes_type ON notes(type);
|
||||
CREATE INDEX idx_notes_isDeleted ON notes(isDeleted);
|
||||
CREATE INDEX idx_notes_dateCreated ON notes(dateCreated);
|
||||
CREATE INDEX idx_notes_dateModified ON notes(dateModified);
|
||||
CREATE INDEX idx_attributes_name ON attributes(name);
|
||||
CREATE INDEX idx_attributes_type ON attributes(type);
|
||||
CREATE INDEX idx_attributes_value ON attributes(value);
|
||||
```
|
||||
|
||||
### Content Processing
|
||||
|
||||
Notes are processed differently based on type:
|
||||
|
||||
```javascript
|
||||
// Content preprocessing by note type
|
||||
function preprocessContent(content, type, mime) {
|
||||
content = normalize(content.toString());
|
||||
|
||||
if (type === "text" && mime === "text/html") {
|
||||
content = stripTags(content);
|
||||
content = content.replace(/ /g, " ");
|
||||
} else if (type === "mindMap" && mime === "application/json") {
|
||||
content = processMindmapContent(content);
|
||||
} else if (type === "canvas" && mime === "application/json") {
|
||||
const canvasData = JSON.parse(content);
|
||||
const textElements = canvasData.elements
|
||||
.filter(el => el.type === "text" && el.text)
|
||||
.map(el => el.text);
|
||||
content = normalize(textElements.join(" "));
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
```
|
||||
|
||||
## Search Result Processing
|
||||
|
||||
### Scoring Algorithm
|
||||
|
||||
Results are scored based on multiple factors:
|
||||
|
||||
```javascript
|
||||
function computeScore(fulltextQuery, highlightedTokens, enableFuzzyMatching) {
|
||||
let score = 0;
|
||||
|
||||
// Title matches get higher score
|
||||
if (this.noteTitle.toLowerCase().includes(fulltextQuery.toLowerCase())) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Path matches (hierarchical context)
|
||||
const pathMatch = this.notePathArray.some(pathNote =>
|
||||
pathNote.title.toLowerCase().includes(fulltextQuery.toLowerCase())
|
||||
);
|
||||
if (pathMatch) score += 5;
|
||||
|
||||
// Attribute matches
|
||||
score += this.attributeMatches * 3;
|
||||
|
||||
// Content snippet quality
|
||||
if (this.contentSnippet && this.contentSnippet.length > 0) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
// Fuzzy match penalty
|
||||
if (enableFuzzyMatching && this.isFuzzyMatch) {
|
||||
score *= 0.8; // 20% penalty for fuzzy matches
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
```
|
||||
|
||||
### Result Merging
|
||||
|
||||
Exact and fuzzy results are carefully merged:
|
||||
|
||||
```javascript
|
||||
function mergeExactAndFuzzyResults(exactResults, fuzzyResults) {
|
||||
// Deduplicate - exact results take precedence
|
||||
const exactNoteIds = new Set(exactResults.map(r => r.noteId));
|
||||
const additionalFuzzyResults = fuzzyResults.filter(r =>
|
||||
!exactNoteIds.has(r.noteId)
|
||||
);
|
||||
|
||||
// Sort within each category
|
||||
exactResults.sort(byScoreAndDepth);
|
||||
additionalFuzzyResults.sort(byScoreAndDepth);
|
||||
|
||||
// CRITICAL: Exact matches always come first
|
||||
return [...exactResults, ...additionalFuzzyResults];
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Search Metrics
|
||||
|
||||
Monitor these performance indicators:
|
||||
|
||||
```javascript
|
||||
// Performance tracking
|
||||
const searchMetrics = {
|
||||
totalQueries: 0,
|
||||
exactSearchTime: 0,
|
||||
fuzzySearchTime: 0,
|
||||
resultCount: 0,
|
||||
cacheHitRate: 0,
|
||||
slowQueries: [] // queries taking > 1 second
|
||||
};
|
||||
```
|
||||
|
||||
### Memory Usage
|
||||
|
||||
Track memory consumption:
|
||||
|
||||
```javascript
|
||||
// Memory monitoring
|
||||
const memoryMetrics = {
|
||||
searchCacheSize: 0,
|
||||
activeSearchContexts: 0,
|
||||
largeContentNotes: 0, // notes > 1MB
|
||||
indexSize: 0
|
||||
};
|
||||
```
|
||||
|
||||
### Query Complexity Analysis
|
||||
|
||||
Identify expensive queries:
|
||||
|
||||
```javascript
|
||||
// Query complexity factors
|
||||
const complexityFactors = {
|
||||
tokenCount: query.split(' ').length,
|
||||
hasRegex: query.includes('%='),
|
||||
hasFuzzy: query.includes('~=') || query.includes('~*'),
|
||||
hasRelationTraversal: query.includes('.relations.'),
|
||||
hasNestedProperties: (query.match(/\./g) || []).length > 2,
|
||||
hasOrderBy: query.includes('orderBy'),
|
||||
estimatedResultSize: 'unknown'
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting Performance Issues
|
||||
|
||||
### Common Performance Problems
|
||||
|
||||
#### Slow Full-Text Search
|
||||
|
||||
```javascript
|
||||
// Diagnosis
|
||||
- Check note content sizes
|
||||
- Verify content type filtering
|
||||
- Monitor regex usage
|
||||
- Review fuzzy search activation
|
||||
|
||||
// Solutions
|
||||
- Enable fast search for attribute-only queries
|
||||
- Add content size limits
|
||||
- Optimize regex patterns
|
||||
- Tune fuzzy search thresholds
|
||||
```
|
||||
|
||||
#### Memory Issues
|
||||
|
||||
```javascript
|
||||
// Diagnosis
|
||||
- Monitor result set sizes
|
||||
- Check for large content processing
|
||||
- Review search context caching
|
||||
- Identify memory leaks
|
||||
|
||||
// Solutions
|
||||
- Add result limits
|
||||
- Implement progressive loading
|
||||
- Clear unused search contexts
|
||||
- Optimize content preprocessing
|
||||
```
|
||||
|
||||
#### High CPU Usage
|
||||
|
||||
```javascript
|
||||
// Diagnosis
|
||||
- Profile fuzzy search operations
|
||||
- Check edit distance calculations
|
||||
- Monitor regex compilation
|
||||
- Review phrase proximity matching
|
||||
|
||||
// Solutions
|
||||
- Increase minimum fuzzy token length
|
||||
- Reduce maximum edit distance
|
||||
- Cache compiled regexes
|
||||
- Limit phrase proximity distance
|
||||
```
|
||||
|
||||
### Debugging Tools
|
||||
|
||||
#### Debug Mode
|
||||
|
||||
Enable search debugging:
|
||||
|
||||
```javascript
|
||||
// Search context with debugging
|
||||
const searchContext = new SearchContext({
|
||||
debug: true // Logs expression parsing and execution
|
||||
});
|
||||
```
|
||||
|
||||
Output includes:
|
||||
|
||||
* Token parsing results
|
||||
* Expression tree structure
|
||||
* Execution timing
|
||||
* Result scoring details
|
||||
|
||||
#### Performance Profiling
|
||||
|
||||
```javascript
|
||||
// Manual performance measurement
|
||||
const startTime = Date.now();
|
||||
const results = searchService.findResultsWithQuery(query, searchContext);
|
||||
const endTime = Date.now();
|
||||
console.log(`Search took ${endTime - startTime}ms for ${results.length} results`);
|
||||
```
|
||||
|
||||
#### Query Analysis
|
||||
|
||||
```javascript
|
||||
// Analyze query complexity
|
||||
function analyzeQuery(query) {
|
||||
return {
|
||||
tokenCount: query.split(/\s+/).length,
|
||||
hasAttributes: /#|\~/.test(query),
|
||||
hasProperties: /note\./.test(query),
|
||||
hasRegex: /%=/.test(query),
|
||||
hasFuzzy: /~[=*]/.test(query),
|
||||
complexity: calculateComplexityScore(query)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration and Tuning
|
||||
|
||||
### Server Configuration
|
||||
|
||||
Relevant settings in `config.ini`:
|
||||
|
||||
```toml
|
||||
# Search-related settings
|
||||
[Search]
|
||||
maxContentSize=2097152 # 2MB content limit
|
||||
minFuzzyTokenLength=3 # Minimum chars for fuzzy
|
||||
maxEditDistance=2 # Edit distance limit
|
||||
resultSufficiencyThreshold=5 # Fuzzy activation threshold
|
||||
enableProgressiveSearch=true # Enable progressive strategy
|
||||
cacheSearchResults=true # Cache frequent searches
|
||||
|
||||
# Performance settings
|
||||
[Performance]
|
||||
searchTimeoutMs=30000 # 30 second search timeout
|
||||
maxSearchResults=1000 # Hard limit on results
|
||||
enableSearchProfiling=false # Performance logging
|
||||
```
|
||||
|
||||
### Runtime Tuning
|
||||
|
||||
Adjust search behavior programmatically:
|
||||
|
||||
```javascript
|
||||
// Dynamic configuration
|
||||
const searchConfig = {
|
||||
maxContentSize: 1024 * 1024, // 1MB for faster processing
|
||||
enableFuzzySearch: false, // Exact only for speed
|
||||
resultLimit: 50, // Smaller result sets
|
||||
useIndexedPropertiesOnly: true // Skip expensive calculations
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices for Performance
|
||||
|
||||
### Query Design
|
||||
|
||||
1. **Start Specific**: Use selective criteria first
|
||||
2. **Limit Results**: Always set reasonable limits
|
||||
3. **Use Indexes**: Prefer indexed properties for ordering
|
||||
4. **Avoid Regex**: Use simple operators when possible
|
||||
5. **Cache Common Queries**: Save frequently used searches
|
||||
|
||||
### System Administration
|
||||
|
||||
1. **Monitor Performance**: Track slow queries and memory usage
|
||||
2. **Regular Maintenance**: Clean up unused notes and attributes
|
||||
3. **Index Optimization**: Ensure database indexes are current
|
||||
4. **Content Management**: Archive or compress large content
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
1. **Test Performance**: Benchmark complex queries
|
||||
2. **Profile Regularly**: Identify performance regressions
|
||||
3. **Optimize Incrementally**: Make small, measured improvements
|
||||
4. **Document Complexity**: Note expensive operations
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Search Extensions
|
||||
|
||||
Extend search functionality with custom expressions:
|
||||
|
||||
```javascript
|
||||
// Custom expression example
|
||||
class CustomDateRangeExp extends Expression {
|
||||
constructor(dateField, startDate, endDate) {
|
||||
super();
|
||||
this.dateField = dateField;
|
||||
this.startDate = startDate;
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
execute(inputNoteSet, executionContext, searchContext) {
|
||||
// Custom logic for date range filtering
|
||||
// with optimized performance characteristics
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Search Result Caching
|
||||
|
||||
Implement result caching for frequent queries:
|
||||
|
||||
```javascript
|
||||
// Simple LRU cache for search results
|
||||
class SearchResultCache {
|
||||
constructor(maxSize = 100) {
|
||||
this.cache = new Map();
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
get(queryKey) {
|
||||
if (this.cache.has(queryKey)) {
|
||||
// Move to end (most recently used)
|
||||
const value = this.cache.get(queryKey);
|
||||
this.cache.delete(queryKey);
|
||||
this.cache.set(queryKey, value);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
set(queryKey, results) {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
// Remove least recently used
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
this.cache.set(queryKey, results);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
* [Search Fundamentals](Search-Fundamentals.md) - Basic concepts and syntax
|
||||
* [Advanced Search Expressions](Advanced-Search-Expressions.md) - Complex query construction
|
||||
* [Search Examples and Use Cases](Search-Examples-and-Use-Cases.md) - Practical applications
|
||||
* [Saved Searches](Saved-Searches.md) - Creating dynamic collections
|
||||
74
docs/User Guide/User Guide/Installation & Setup/1_Server Installation.md
vendored
Normal file
74
docs/User Guide/User Guide/Installation & Setup/1_Server Installation.md
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# Server Installation
|
||||
Running Trilium on a server lets you access your notes from any device through a web browser and enables synchronization between multiple Trilium instances. This guide covers the different ways to install and configure Trilium on your server.
|
||||
|
||||
## Choose Your Installation Method
|
||||
|
||||
The easiest way to get started is with Docker, which works on most systems and architectures. If you prefer not to manage your own server, PikaPods offers managed hosting.
|
||||
|
||||
**Recommended approaches:**
|
||||
|
||||
* [Docker Installation](1_Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md) - Works on AMD64 and ARM architectures
|
||||
* [PikaPods managed hosting](https://www.pikapods.com/pods?run=trilium-next) - No server management required
|
||||
* [Packaged Server Installation](1_Server%20Installation/1.%20Installing%20the%20server/Packaged%20version%20for%20Linux.md) - Native Linux packages
|
||||
|
||||
**Advanced options:**
|
||||
|
||||
* [Manual Installation](1_Server%20Installation/1.%20Installing%20the%20server/Manually.md) - Full control over the setup
|
||||
* [Kubernetes](1_Server%20Installation/1.%20Installing%20the%20server/Using%20Kubernetes.md) - For container orchestration
|
||||
* [NixOS Module](1_Server%20Installation/1.%20Installing%20the%20server/On%20NixOS.md) - Declarative configuration
|
||||
|
||||
All server installations include both desktop and mobile web interfaces.
|
||||
|
||||
## Configuration
|
||||
|
||||
Trilium stores its configuration in a `config.ini` file located in the [data directory](#root/dvbMBRXYMM2G). To customize your installation, copy the sample configuration file and modify it:
|
||||
|
||||
```sh
|
||||
cp config-sample.ini config.ini
|
||||
```
|
||||
|
||||
You can also use environment variables instead of the config file. This is particularly useful for Docker deployments. See the [configuration guide](#root/SneMubD5wTR6) for all available options.
|
||||
|
||||
### Changing the Data Directory
|
||||
|
||||
To store Trilium's data (database, config, backups) in a custom location, set the `TRILIUM_DATA_DIR` environment variable:
|
||||
|
||||
```sh
|
||||
export TRILIUM_DATA_DIR=/path/to/your/trilium-data
|
||||
```
|
||||
|
||||
### Upload Size Limits
|
||||
|
||||
By default, Trilium limits file uploads to 250MB. You can adjust this limit based on your needs:
|
||||
|
||||
```sh
|
||||
# Increase limit to 450MB
|
||||
export MAX_ALLOWED_FILE_SIZE_MB=450
|
||||
|
||||
# Remove limit entirely (use with caution)
|
||||
export TRILIUM_NO_UPLOAD_LIMIT=true
|
||||
```
|
||||
|
||||
### Disabling Authentication
|
||||
|
||||
See <a class="reference-link" href="1_Server%20Installation/Authentication.md">Authentication</a>.
|
||||
|
||||
## Reverse Proxy Setup
|
||||
|
||||
If you want to access Trilium through a domain name or alongside other web services, you'll need to configure a reverse proxy. Here's a basic nginx configuration:
|
||||
|
||||
```nginx
|
||||
location /trilium/ {
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# Allow larger file uploads (in server block)
|
||||
client_max_body_size 0; # 0 = unlimited
|
||||
```
|
||||
|
||||
For Apache configuration, see the [Apache proxy setup](1_Server%20Installation/2.%20Reverse%20proxy/Apache.md) guide.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Manually
|
||||
> [!WARNING]
|
||||
> This page describes manually installing Trilium on your server. **Note that this is a not well supported way to install Trilium, problems may appear, information laid out here is quite out of date. It is recommended to use either** <a class="reference-link" href="Using%20Docker.md">Docker Server Installation</a> **or** <a class="reference-link" href="Packaged%20version%20for%20Linux.md">Packaged server installation</a>**.**
|
||||
|
||||
## Requirements
|
||||
|
||||
Trilium is a node.js application. Supported (tested) version of node.js is latest 14.X.X and 16.X.X. Trilium might work with older versions as well.
|
||||
|
||||
You can check your node version with this command (node.js needs to be installed):
|
||||
|
||||
```
|
||||
node --version
|
||||
```
|
||||
|
||||
If your Linux distribution has only an outdated version of node.js, you can take a look at the installation instruction on node.js website, which covers most popular distributions.
|
||||
|
||||
### Dependencies
|
||||
|
||||
There are some dependencies required. You can see command for Debian and its derivatives (like Ubuntu) below:
|
||||
|
||||
```
|
||||
sudo apt install libpng16-16 libpng-dev pkg-config autoconf libtool build-essential nasm libx11-dev libxkbfile-dev
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Download
|
||||
|
||||
You can either download source code zip/tar from [https://github.com/TriliumNext/Trilium/releases/latest](https://github.com/TriliumNext/Trilium/releases/latest).
|
||||
|
||||
For the latest version including betas, clone Git repository **from** `main` **branch** with:
|
||||
|
||||
```
|
||||
git clone -b main https://github.com/triliumnext/trilium.git
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
cd trilium
|
||||
|
||||
# download all node dependencies
|
||||
npm install
|
||||
|
||||
# make sure the better-sqlite3 binary is there
|
||||
npm rebuild
|
||||
|
||||
# bundles & minifies frontend JavaScript
|
||||
npm run webpack
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
cd trilium
|
||||
|
||||
# using nohup to make sure trilium keeps running after user logs out
|
||||
nohup TRILIUM_ENV=dev node src/www &
|
||||
```
|
||||
|
||||
The application by default starts up on port 8080, so you can open your browser and navigate to [http://localhost:8080](http://localhost:8080) to access Trilium (replace "localhost" with your hostname).
|
||||
|
||||
## TLS
|
||||
|
||||
Don't forget to [configure TLS](../TLS%20Configuration.md) which is required for secure usage!
|
||||
@@ -0,0 +1,22 @@
|
||||
# Multiple server instances
|
||||
Trilium does not support multiple users. In order to have two or more persons with their own set of notes, multiple server instances must be set up. It is also not possible to use multiple [sync](#root/KFhm9yCthQOh) servers.
|
||||
|
||||
To allow multiple server instances on a single physical server:
|
||||
|
||||
* For <a class="reference-link" href="Packaged%20version%20for%20Linux.md">Packaged version for Linux</a> or <a class="reference-link" href="Manually.md">Manually</a>, if starting the server manually just specify a different port and data directory per instance:
|
||||
|
||||
```
|
||||
TRILIUM_NETWORK_PORT=8080 TRILIUM_DATA_DIR=/path/to/your/data-dir-A /opt/trilium/trilium.sh
|
||||
```
|
||||
|
||||
For a second instance:
|
||||
|
||||
```
|
||||
TRILIUM_NETWORK_PORT=8081 TRILIUM_DATA_DIR=/path/to/your/data-dir-B /opt/trilium/trilium.sh
|
||||
```
|
||||
|
||||
If using `systemd`, then set the [environment variables in the service configuration](https://serverfault.com/questions/413397/how-to-set-environment-variable-in-systemd-service).
|
||||
* For <a class="reference-link" href="Using%20Docker.md">Using Docker</a>, simply use two different containers, each with their own port binding and data directory.
|
||||
* For <a class="reference-link" href="On%20NixOS.md">On NixOS</a>, the only possible way is to use Docker OCI containers or at least one NixOS container with its own service definition.
|
||||
|
||||
For support or additional context, see the related [GitHub Discussion](https://github.com/orgs/TriliumNext/discussions/1642#discussioncomment-12768808).
|
||||
@@ -0,0 +1,25 @@
|
||||
# On NixOS
|
||||
This page describes configuring the Trilium module included in NixOS.
|
||||
|
||||
## Requirements
|
||||
|
||||
[NixOS](https://nixos.org/) installation.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add this to your `configuration.nix`:
|
||||
|
||||
```
|
||||
services.trilium-server.enable = true;
|
||||
|
||||
# default data directory: /var/lib/trilium
|
||||
#services.trilium-server.dataDir = "/var/lib/trilium-sync-server";
|
||||
|
||||
# default bind address: 127.0.0.1, port 8080
|
||||
#services.trilium-server.host = "0.0.0.0";
|
||||
#services.trilium-server.port = 12783;
|
||||
```
|
||||
|
||||
Uncomment any option you would like to change.
|
||||
|
||||
See the [NixOS options list](https://search.nixos.org/options?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=trilium-server) for more options (including nginx reverse proxy configuration).
|
||||
@@ -0,0 +1,181 @@
|
||||
# Packaged version for Linux
|
||||
This is essentially Trilium sources + node modules + node.js runtime packaged into one 7z file.
|
||||
|
||||
## Steps
|
||||
|
||||
* SSH into your server
|
||||
* use `wget` (or `curl`) to download latest `TriliumNotes-Server-[VERSION]-linux-x64.tar.xz` (copy link from [release page](https://github.com/TriliumNext/Trilium/releases), notice `-Server` suffix) on your server.
|
||||
* unpack the archive, e.g. using `tar -xf -d TriliumNotes-Server-[VERSION]-linux-x64.tar.xz`
|
||||
* `cd trilium-linux-x64-server`
|
||||
* `./trilium.sh`
|
||||
* you can open the browser and open http://\[your-server-hostname\]:8080 and you should see Trilium initialization page
|
||||
|
||||
The problem with above steps is that once you close the SSH connection, the Trilium process is terminated. To avoid that, you have two options:
|
||||
|
||||
* Kill it (with e.g. <kbd>Ctrl</kbd> + <kbd>C</kbd>) and run again like this: `nohup ./trilium.sh &`. (nohup keeps the process running in the background, `&` runs it in the background)
|
||||
* Configure systemd to automatically run Trilium in the background on every boot
|
||||
|
||||
## Configure Trilium to auto-run on boot with systemd
|
||||
|
||||
* After downloading, extract and move Trilium:
|
||||
|
||||
```
|
||||
tar -xvf TriliumNotes-Server-[VERSION]-linux-x64.tar.xz
|
||||
sudo mv trilium-linux-x64-server /opt/trilium
|
||||
```
|
||||
|
||||
* Create the service:
|
||||
|
||||
```
|
||||
sudo nano /etc/systemd/system/trilium.service
|
||||
```
|
||||
|
||||
* Paste this into the file (replace the user and group as needed):
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Trilium Daemon
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
User=xxx
|
||||
Group=xxx
|
||||
Type=simple
|
||||
ExecStart=/opt/trilium/trilium.sh
|
||||
WorkingDirectory=/opt/trilium/
|
||||
|
||||
TimeoutStopSec=20
|
||||
# KillMode=process leads to error, according to https://www.freedesktop.org/software/systemd/man/systemd.kill.html
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
* Save the file (CTRL-S) and exit (CTRL-X)
|
||||
* Enable and launch the service:
|
||||
|
||||
```
|
||||
sudo systemctl enable --now -q trilium
|
||||
```
|
||||
|
||||
* You can now open a browser to http://\[your-server-hostname\]:8080 and you should see the Trilium initialization page.
|
||||
|
||||
## Simple Autoupdate for Server
|
||||
|
||||
Run as the same User Trilium runs
|
||||
|
||||
if you run as root please remove 'sudo' from the commands
|
||||
|
||||
requires "jq" `apt install jq`
|
||||
|
||||
It will stop the service above, overwrite everything (i expect no config.ini), and start service It also creates a version file in the Trilium directory so it updates only with a newer Version
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
REPO="TriliumNext/Trilium"
|
||||
PATTERN="TriliumNotes-Server-.*-linux-x64.tar.xz"
|
||||
DOWNLOAD_DIR="/var/tmp/trilium_download"
|
||||
OUTPUT_DIR="/opt/trilium"
|
||||
SERVICE_NAME="trilium"
|
||||
VERSION_FILE="$OUTPUT_DIR/version.txt"
|
||||
|
||||
# Ensure dependencies are installed
|
||||
command -v curl >/dev/null 2>&1 || { echo "Error: curl is required"; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo "Error: jq is required"; exit 1; }
|
||||
command -v tar >/dev/null 2>&1 || { echo "Error: tar is required"; exit 1; }
|
||||
|
||||
# Create download directory
|
||||
mkdir -p "$DOWNLOAD_DIR" || { echo "Error: Cannot create $DOWNLOAD_DIR"; exit 1; }
|
||||
|
||||
# Get the latest release version
|
||||
LATEST_VERSION=$(curl -sL https://api.github.com/repos/$REPO/releases/latest | jq -r '.tag_name')
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "Error: Could not fetch latest release version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check current installed version (from version.txt or existing tarball)
|
||||
CURRENT_VERSION=""
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
CURRENT_VERSION=$(cat "$VERSION_FILE")
|
||||
elif [ -f "$DOWNLOAD_DIR/TriliumNotes-Server-$LATEST_VERSION-linux-x64.tar.xz" ]; then
|
||||
CURRENT_VERSION="$LATEST_VERSION"
|
||||
fi
|
||||
|
||||
# Compare versions
|
||||
if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then
|
||||
echo "Latest version ($LATEST_VERSION) is already installed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Download the latest release
|
||||
LATEST_URL=$(curl -sL https://api.github.com/repos/$REPO/releases/latest | jq -r ".assets[] | select(.name | test(\"$PATTERN\")) | .browser_download_url")
|
||||
if [ -z "$LATEST_URL" ]; then
|
||||
echo "Error: No asset found matching pattern '$PATTERN'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE_NAME=$(basename "$LATEST_URL")
|
||||
FILE_PATH="$DOWNLOAD_DIR/$FILE_NAME"
|
||||
|
||||
# Download if not already present
|
||||
if [ -f "$FILE_PATH" ]; then
|
||||
echo "Latest release $FILE_NAME already downloaded"
|
||||
else
|
||||
curl -LO --output-dir "$DOWNLOAD_DIR" "$LATEST_URL" || { echo "Error: Download failed"; exit 1; }
|
||||
echo "Downloaded $FILE_NAME to $DOWNLOAD_DIR"
|
||||
fi
|
||||
|
||||
# Extract the tarball
|
||||
EXTRACT_DIR="$DOWNLOAD_DIR/extracted"
|
||||
mkdir -p "$EXTRACT_DIR"
|
||||
tar -xJf "$FILE_PATH" -C "$EXTRACT_DIR" || { echo "Error: Extraction failed"; exit 1; }
|
||||
|
||||
# Find the extracted directory (e.g., TriliumNotes-Server-0.97.2-linux-x64)
|
||||
INNER_DIR=$(find "$EXTRACT_DIR" -maxdepth 1 -type d -name "TriliumNotes-Server-*-linux-x64" | head -n 1)
|
||||
if [ -z "$INNER_DIR" ]; then
|
||||
echo "Error: Could not find extracted directory matching TriliumNotes-Server-*-linux-x64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop the trilium-server service
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
echo "Stopping $SERVICE_NAME service..."
|
||||
sudo systemctl stop "$SERVICE_NAME" || { echo "Error: Failed to stop $SERVICE_NAME"; exit 1; }
|
||||
fi
|
||||
|
||||
# Copy contents to /opt/trilium, overwriting existing files
|
||||
echo "Copying contents from $INNER_DIR to $OUTPUT_DIR..."
|
||||
sudo mkdir -p "$OUTPUT_DIR"
|
||||
sudo cp -r "$INNER_DIR"/* "$OUTPUT_DIR"/ || { echo "Error: Copy failed"; exit 1; }
|
||||
echo "$LATEST_VERSION" | sudo tee "$VERSION_FILE" >/dev/null
|
||||
echo "Files copied to $OUTPUT_DIR"
|
||||
|
||||
# Start the trilium-server service
|
||||
echo "Starting $SERVICE_NAME service..."
|
||||
sudo systemctl start "$SERVICE_NAME" || { echo "Error: Failed to start $SERVICE_NAME"; exit 1; }
|
||||
|
||||
# Clean up
|
||||
rm -rf "$EXTRACT_DIR"
|
||||
echo "Cleanup complete. Trilium updated to $LATEST_VERSION."
|
||||
```
|
||||
|
||||
## Common issues
|
||||
|
||||
### Outdated glibc
|
||||
|
||||
```
|
||||
Error: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /var/www/virtual/.../node_modules/@mlink/scrypt/build/Release/scrypt.node)
|
||||
at Object.Module._extensions..node (module.js:681:18)
|
||||
at Module.load (module.js:565:32)
|
||||
at tryModuleLoad (module.js:505:12)
|
||||
```
|
||||
|
||||
If you get an error like this, you need to either upgrade your glibc (typically by upgrading to up-to-date distribution version) or use some other [server installation](../../1_Server%20Installation.md) method.
|
||||
|
||||
## TLS
|
||||
|
||||
Don't forget to [configure TLS](../TLS%20Configuration.md), which is required for secure usage!
|
||||
@@ -0,0 +1,241 @@
|
||||
# Using Docker
|
||||
Official docker images are published on docker hub for **AMD64**, **ARMv7** and **ARM64/v8**: [https://hub.docker.com/r/triliumnext/trilium/](https://hub.docker.com/r/triliumnext/trilium/)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Ensure Docker is installed on your system.
|
||||
|
||||
If you need help installing Docker, reference the [Docker Installation Docs](https://docs.docker.com/engine/install/)
|
||||
|
||||
**Note:** Trilium's Docker container requires root privileges to operate correctly.
|
||||
|
||||
> [!WARNING]
|
||||
> If you're using a SMB/CIFS share or folder as your Trilium data directory, [you'll need](https://github.com/TriliumNext/Notes/issues/415#issuecomment-2344824400) to add the mount options of `nobrl` and `noperm` when mounting your SMB share.
|
||||
|
||||
## Running with Docker Compose
|
||||
|
||||
### Grab the latest docker-compose.yml:
|
||||
|
||||
```
|
||||
wget https://raw.githubusercontent.com/TriliumNext/Trilium/master/docker-compose.yml
|
||||
```
|
||||
|
||||
Optionally, edit the `docker-compose.yml` file to configure the container settings prior to starting it. Unless configured otherwise, the data directory will be `~/trilium-data` and the container will be accessible at port 8080.
|
||||
|
||||
### Start the container:
|
||||
|
||||
Run the following command to start the container in the background:
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Running without Docker Compose / Further Configuration
|
||||
|
||||
### Pulling the Docker Image
|
||||
|
||||
To pull the image, use the following command, replacing `[VERSION]` with the desired version or tag, such as `v0.91.6` or just `latest`. (See published tag names at [https://hub.docker.com/r/triliumnext/trilium/tags](https://hub.docker.com/r/triliumnext/trilium/tags).):
|
||||
|
||||
```
|
||||
docker pull triliumnext/trilium:v0.91.6
|
||||
```
|
||||
|
||||
**Warning:** Avoid using the "latest" tag, as it may automatically upgrade your instance to a new minor version, potentially disrupting sync setups or causing other issues.
|
||||
|
||||
### Preparing the Data Directory
|
||||
|
||||
Trilium requires a directory on the host system to store its data. This directory must be mounted into the Docker container with write permissions.
|
||||
|
||||
### Running the Docker Container
|
||||
|
||||
#### Local Access Only
|
||||
|
||||
Run the container to make it accessible only from the localhost. This setup is suitable for testing or when using a proxy server like Nginx or Apache.
|
||||
|
||||
```
|
||||
sudo docker run -t -i -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]
|
||||
```
|
||||
|
||||
1. Verify the container is running using `docker ps`.
|
||||
2. Access Trilium via a web browser at `127.0.0.1:8080`.
|
||||
|
||||
#### Local Network Access
|
||||
|
||||
To make the container accessible only on your local network, first create a new Docker network:
|
||||
|
||||
```
|
||||
docker network create -d macvlan -o parent=eth0 --subnet 192.168.2.0/24 --gateway 192.168.2.254 --ip-range 192.168.2.252/27 mynet
|
||||
```
|
||||
|
||||
Then, run the container with the network settings:
|
||||
|
||||
```
|
||||
docker run --net=mynet -d -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:-latest
|
||||
```
|
||||
|
||||
To set a different user ID (UID) and group ID (GID) for the saved data, use the `USER_UID` and `USER_GID` environment variables:
|
||||
|
||||
```
|
||||
docker run --net=mynet -d -p 127.0.0.1:8080:8080 -e "USER_UID=1001" -e "USER_GID=1001" -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:-latest
|
||||
```
|
||||
|
||||
Find the local IP address using `docker inspect [container_name]` and access the service from devices on the local network.
|
||||
|
||||
```
|
||||
docker ps
|
||||
docker inspect [container_name]
|
||||
```
|
||||
|
||||
#### Global Access
|
||||
|
||||
To allow access from any IP address, run the container as follows:
|
||||
|
||||
```
|
||||
docker run -d -p 0.0.0.0:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]
|
||||
```
|
||||
|
||||
Stop the container with `docker stop <CONTAINER ID>`, where the container ID is obtained from `docker ps`.
|
||||
|
||||
### Custom Data Directory
|
||||
|
||||
For a custom data directory, use:
|
||||
|
||||
```
|
||||
-v ~/YourOwnDirectory:/home/node/trilium-data triliumnext/trilium:[VERSION]
|
||||
```
|
||||
|
||||
If you want to run your instance in a non-default way, please use the volume switch as follows: `-v ~/YourOwnDirectory:/home/node/trilium-data triliumnext/trilium:<VERSION>`. It is important to be aware of how Docker works for volumes, with the first path being your own and the second the one to virtually bind to. [https://docs.docker.com/storage/volumes/](https://docs.docker.com/storage/volumes/) The path before the colon is the host directory, and the path after the colon is the container's path. More details can be found in the [Docker Volumes Documentation](https://docs.docker.com/storage/volumes/).
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
1. [Nginx](../2.%20Reverse%20proxy/Nginx.md)
|
||||
2. [Apache](../2.%20Reverse%20proxy/Apache.md)
|
||||
|
||||
### Note on --user Directive
|
||||
|
||||
The `--user` directive is unsupported. Instead, use the `USER_UID` and `USER_GID` environment variables to set the appropriate user and group IDs.
|
||||
|
||||
### Note on timezones
|
||||
|
||||
If you are having timezone issues and you are not using docker-compose, you may need to add a `TZ` environment variable with the [TZ identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) of your local timezone.
|
||||
|
||||
## Rootless Docker Image
|
||||
|
||||
> [!NOTE]
|
||||
> Please keep in mind that the data directory is at `/home/trilium/trilium-data` instead of the typical `/home/node/trilium-data`. This is because a new user is created and used to run Trilium within the rootless containers.
|
||||
|
||||
If you would prefer to run Trilium without having to run the Docker container as `root`, you can use either of the provided Debian (default) and Alpine-based images with the `rootless` tag.
|
||||
|
||||
_**If you're unsure, stick to the “rootful” Docker image referenced above.**_
|
||||
|
||||
Below are some commands to pull the rootless images:
|
||||
|
||||
```
|
||||
# For Debian-based image
|
||||
docker pull triliumnext/trilium:rootless
|
||||
|
||||
# For Alpine-based image
|
||||
docker pull triliumnext/trilium:rootless-alpine
|
||||
```
|
||||
|
||||
### Why Rootless?
|
||||
|
||||
Running containers as non-root is a security best practice that reduces the potential impact of container breakouts. If an attacker manages to escape the container, they'll only have the permissions of the non-root user instead of full root access to the host.
|
||||
|
||||
### How It Works
|
||||
|
||||
The rootless Trilium image:
|
||||
|
||||
1. Creates a non-root user (`trilium`) during build time
|
||||
2. Configures the application to run as this non-root user
|
||||
3. Allows runtime customization of the user's UID/GID via Docker's `--user` flag
|
||||
4. Does not require a separate Docker `entrypoint` script
|
||||
|
||||
### Usage
|
||||
|
||||
#### **Using docker-compose (Recommended)**
|
||||
|
||||
```
|
||||
# Run with default UID/GID (1000:1000)
|
||||
docker-compose -f docker-compose.rootless.yml up -d
|
||||
|
||||
# Run with custom UID/GID (e.g., match your host user)
|
||||
TRILIUM_UID=$(id -u) TRILIUM_GID=$(id -g) docker-compose -f docker-compose.rootless.yml up -d
|
||||
|
||||
# Specify a custom data directory
|
||||
TRILIUM_DATA_DIR=/path/to/your/data TRILIUM_UID=$(id -u) TRILIUM_GID=$(id -g) docker-compose -f docker-compose.rootless.yml up -d
|
||||
|
||||
```
|
||||
|
||||
#### **Using Docker CLI**
|
||||
|
||||
```
|
||||
# Build the image
|
||||
docker build -t triliumnext/trilium:rootless -f apps/server/Dockerfile.rootless .
|
||||
|
||||
# Run with default UID/GID (1000:1000)
|
||||
docker run -d --name trilium -p 8080:8080 -v ~/trilium-data:/home/trilium/trilium-data triliumnext/trilium:rootless
|
||||
|
||||
# Run with custom UID/GID
|
||||
docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-data:/home/trilium/trilium-data triliumnext/trilium:rootless
|
||||
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
* `TRILIUM_UID`: UID to use for the container process (passed to Docker's `--user` flag)
|
||||
* `TRILIUM_GID`: GID to use for the container process (passed to Docker's `--user` flag)
|
||||
* `TRILIUM_DATA_DIR`: Path to the data directory inside the container (default: `/home/node/trilium-data`)
|
||||
|
||||
For a complete list of configuration environment variables (network settings, authentication, sync, etc.), see <a class="reference-link" href="#root/1CEQXvOOO4EK">Configuration (config.ini or environment variables)</a>.
|
||||
|
||||
### Volume Permissions
|
||||
|
||||
If you encounter permission issues with the data volume, ensure that:
|
||||
|
||||
1. The host directory has appropriate permissions for the UID/GID you're using
|
||||
2. You're setting both `TRILIUM_UID` and `TRILIUM_GID` to match the owner of the host directory
|
||||
|
||||
```
|
||||
# For example, if your data directory is owned by UID 1001 and GID 1001:
|
||||
TRILIUM_UID=1001 TRILIUM_GID=1001 docker-compose -f docker-compose.rootless.yml up -d
|
||||
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
* The container starts with a specific UID/GID which can be customized at runtime
|
||||
* Unlike the traditional setup, this approach does not use a separate entrypoint script with `usermod`/`groupmod` commands
|
||||
* The container cannot modify its own UID/GID at runtime, which is a security feature of rootless containers
|
||||
|
||||
### Available Rootless Images
|
||||
|
||||
Two rootless variants are provided:
|
||||
|
||||
1. **Debian-based** (default): Uses the Debian Bullseye Slim base image
|
||||
* Dockerfile: `apps/server/Dockerfile.rootless`
|
||||
* Recommended for most users
|
||||
2. **Alpine-based**: Uses the Alpine base image for smaller size
|
||||
* Dockerfile: `apps/server/Dockerfile.alpine.rootless`
|
||||
* Smaller image size, but may have compatibility issues with some systems
|
||||
|
||||
### Building Custom Rootless Images
|
||||
|
||||
If you would prefer, you can also customize the UID/GID at build time:
|
||||
|
||||
```
|
||||
# For Debian-based image with custom UID/GID
|
||||
docker build --build-arg USER=myuser --build-arg UID=1001 --build-arg GID=1001 \
|
||||
-t triliumnext/trilium:rootless-custom -f apps/server/Dockerfile.rootless .
|
||||
|
||||
# For Alpine-based image with custom UID/GID
|
||||
docker build --build-arg USER=myuser --build-arg UID=1001 --build-arg GID=1001 \
|
||||
-t triliumnext/trilium:alpine-rootless-custom -f apps/server/Dockerfile.alpine.rootless .
|
||||
|
||||
```
|
||||
|
||||
Available build arguments:
|
||||
|
||||
* `USER`: Username for the non-root user (default: trilium)
|
||||
* `UID`: User ID for the non-root user (default: 1000)
|
||||
* `GID`: Group ID for the non-root user (default: 1000)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Using Kubernetes
|
||||
As Trilium can be run in Docker it also can be deployed in Kubernetes. You can either use our Helm chart, a community Helm chart, or roll your own Kubernetes deployment.
|
||||
|
||||
The recommended way is to use a Helm chart.
|
||||
|
||||
## Root privileges
|
||||
|
||||
> [!NOTE]
|
||||
> The Trilium container at this time needs to be run with root privileges. It will swap to UID and GID `1000:1000` to run the `node` process after execution though, so the main process doesn't run with root privileges.
|
||||
|
||||
The Trilium docker container needs to be run with root privileges. The node process inside the container will be started with reduced privileges (uid:gid 1000:1000) after some initialization logic. Please make sure that you don't use a security context (PodSecurityContext) which changes the user ID. To use a different uid:gid for file storage and the application, please use the `USER_UID` & `USER_GID` environment variables.
|
||||
|
||||
The docker image will also fix the permissions of `/home/node` so you don't have to use an init container.
|
||||
|
||||
## Helm Charts
|
||||
|
||||
[Official Helm chart](https://github.com/TriliumNext/helm-charts) from TriliumNext Unofficial helm chart by [ohdearaugustin](https://github.com/ohdearaugustin): [https://github.com/ohdearaugustin/charts](https://github.com/ohdearaugustin/charts)
|
||||
|
||||
## Adding a Helm repository
|
||||
|
||||
Below is an example of how
|
||||
|
||||
```
|
||||
helm repo add trilium https://triliumnext.github.io/helm-charts
|
||||
"trilium" has been added to your repositories
|
||||
```
|
||||
|
||||
## How to install a chart
|
||||
|
||||
After reviewing the [`values.yaml`](https://github.com/TriliumNext/helm-charts/blob/main/charts/trilium/values.yaml) from the Helm chart, modifying as required and then creating your own:
|
||||
|
||||
```
|
||||
helm install --create-namespace --namespace trilium trilium trilium/trilium -f values.yaml
|
||||
```
|
||||
|
||||
For more information on using Helm, please refer to the Helm documentation, or create a Discussion in the TriliumNext GitHub Organization.
|
||||
81
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.md
vendored
Normal file
81
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Apache.md
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# Apache
|
||||
I've assumed you have created a DNS A record for `trilium.yourdomain.com` that you want to use for your Trilium server.
|
||||
|
||||
1. Download docker image and create container
|
||||
|
||||
```
|
||||
docker pull triliumnext/trilium:[VERSION]
|
||||
docker create --name trilium -t -p 127.0.0.1:8080:8080 -v ~/trilium-data:/home/node/trilium-data triliumnext/trilium:[VERSION]
|
||||
```
|
||||
2. Configure Apache proxy and websocket proxy
|
||||
|
||||
1. Enable apache proxy modules
|
||||
|
||||
```
|
||||
a2enmod ssl
|
||||
a2enmod proxy
|
||||
a2enmod proxy_http
|
||||
a2enmod proxy_wstunnel
|
||||
```
|
||||
2. Create a new let's encrypt certificate
|
||||
|
||||
```
|
||||
sudo certbot certonly -d trilium.mydomain.com
|
||||
```
|
||||
|
||||
Choose standalone (2) and note the location of the created certificates (typically /etc/letsencrypt/live/...)
|
||||
3. Create a new virtual host file for apache (you may want to use `apachectl -S` to determine the server root location, mine is /etc/apache2)
|
||||
|
||||
```
|
||||
sudo nano /etc/apache2/sites-available/trilium.yourdomain.com.conf
|
||||
```
|
||||
|
||||
Paste (and customize) the following text into the configuration file
|
||||
|
||||
```
|
||||
|
||||
ServerName http://trilium.yourdomain.com
|
||||
RewriteEngine on
|
||||
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent]
|
||||
|
||||
|
||||
ServerName https://trilium.yourdomain.com
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Connection} Upgrade [NC]
|
||||
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||
RewriteRule /(.*) ws://localhost:8080/$1 [P,L]
|
||||
AllowEncodedSlashes NoDecode
|
||||
ProxyPass / http://localhost:8080/ nocanon
|
||||
ProxyPassReverse / http://localhost:8080/
|
||||
SSLCertificateFile /etc/letsencrypt/live/trilium.yourdomain.com/fullchain.pem
|
||||
SSLCertificateKeyFile /etc/letsencrypt/live/trilium.yourdomain.com/privkey.pem
|
||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||
|
||||
```
|
||||
4. Enable the virtual host with `sudo a2ensite trilium.yourdomain.com.conf`
|
||||
5. Reload apache2 with `sudo systemctl reload apache2`
|
||||
3. Create and enable a systemd service to start the docker container on boot
|
||||
|
||||
1. Create a new empty file called `/lib/systemd/system/trilium.service` with the contents
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Trilium Server
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/docker start -a trilium
|
||||
ExecStop=/usr/bin/docker stop -t 2 trilium
|
||||
|
||||
[Install]
|
||||
WantedBy=local.target
|
||||
```
|
||||
2. Install, enable and start service
|
||||
|
||||
```
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable trilium.service
|
||||
sudo systemctl start trilium.service
|
||||
```
|
||||
76
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Nginx.md
vendored
Normal file
76
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/2. Reverse proxy/Nginx.md
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# Nginx
|
||||
Configure Nginx proxy and HTTPS. The operating system here is Ubuntu 18.04.
|
||||
|
||||
1. Download Nginx and remove Apache2
|
||||
|
||||
```
|
||||
sudo apt-get install nginx
|
||||
sudo apt-get remove apache2
|
||||
```
|
||||
2. Create configure file
|
||||
|
||||
```
|
||||
cd /etc/nginx/conf.d
|
||||
vim default.conf
|
||||
```
|
||||
3. Fill the file with the context shown below, part of the setting show be changed. Then you can enjoy your web with HTTPS forced and proxy.
|
||||
|
||||
```
|
||||
# This part configures, where your Trilium server is running
|
||||
upstream trilium {
|
||||
zone trilium 64k;
|
||||
server 127.0.0.1:8080; # change it to a different hostname and port if non-default is used
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
# This part is for proxy and HTTPS configure
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name trilium.example.net; #change trilium.example.net to your domain without HTTPS or HTTP.
|
||||
ssl_certificate /etc/ssl/note/example.crt; #change /etc/ssl/note/example.crt to your path of crt file.
|
||||
ssl_certificate_key /etc/ssl/note/example.net.key; #change /etc/ssl/note/example.net.key to your path of key file.
|
||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
||||
ssl_prefer_server_ciphers on;
|
||||
access_log /var/log/nginx/access.log; #check the path of access.log, if it doesn't fit your file, change it
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://trilium;
|
||||
proxy_read_timeout 90;
|
||||
}
|
||||
}
|
||||
|
||||
# This part is for HTTPS forced
|
||||
server {
|
||||
listen 80;
|
||||
server_name trilium.example.net; # change to your domain
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
```
|
||||
4. Alternatively if you want to serve the instance under a different path (useful e.g. if you want to serve multiple instances), update the location block like so:
|
||||
|
||||
* update the location with your desired path (make sure to not leave a trailing slash "/", if your `proxy_pass` does not end on a slash as well)
|
||||
* add the `proxy_cookie_path` directive with the same path: this allows you to stay logged in at multiple instances at the same time.
|
||||
|
||||
```
|
||||
location /trilium/instance-one {
|
||||
rewrite /trilium/instance-one/(.*) /$1 break;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://trilium;
|
||||
proxy_cookie_path / /trilium/instance-one
|
||||
proxy_read_timeout 90;
|
||||
}
|
||||
|
||||
```
|
||||
38
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/Authentication.md
vendored
Normal file
38
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/Authentication.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Authentication
|
||||
## Disabling authentication
|
||||
|
||||
If you are running Trilium on `localhost` only or if authentication is handled by another component, you can disable Trilium’s authentication by adding the following to `config.ini`:
|
||||
|
||||
```
|
||||
[General]
|
||||
noAuthentication=true
|
||||
```
|
||||
|
||||
Disabling authentication will bypass even the <a class="reference-link" href="Multi-Factor%20Authentication.md">Multi-Factor Authentication</a> since v0.94.1.
|
||||
|
||||
## Understanding how the session works
|
||||
|
||||
Once logged into Trilium, the application will store this information about the login into a cookie on the browser, but also as a session on the server.
|
||||
|
||||
If “Remember me” is checked, then the login will expire in 21 days. This period can be adjusted by modifying the `Session.cookieMaxAge` value in `config.ini`. For example, to have the session expire in one day:
|
||||
|
||||
```
|
||||
[Session]
|
||||
cookieMaxAge=86400
|
||||
```
|
||||
|
||||
When “Remember me” is unchecked, the behavior is different. At client/browser level the authentication does not have any expiration date, but it will be automatically cleared as soon as the user closes the browser. Nevertheless, the server will also dismiss this authentication in around 24 hours from the _last interaction with the application_.
|
||||
|
||||
## Viewing active sessions
|
||||
|
||||
The login sessions are now stored in the same <a class="reference-link" href="#root/lvXOQ00dcRlk">Database</a> as the user data. In order to view which sessions are active, open the <a class="reference-link" href="#root/hDJ4mPkZJQ4E">SQL Console</a> and run the following query:
|
||||
|
||||
```
|
||||
SELECT * FROM sessions
|
||||
```
|
||||
|
||||
Expired sessions are periodically cleaned by the server, generally an hourly interval.
|
||||
|
||||
## See also
|
||||
|
||||
* <a class="reference-link" href="Multi-Factor%20Authentication.md">Multi-Factor Authentication</a>
|
||||
@@ -0,0 +1,67 @@
|
||||
# Multi-Factor Authentication
|
||||
Multi-factor authentication (MFA) is a security process that requires users to provide two or more verification factors to gain access to a system, application, or account. This adds an extra layer of protection beyond just using a password.
|
||||
|
||||
By requiring more than one verification method, MFA helps reduce the risk of unauthorized access, even if someone has obtained your password. It’s highly recommended for securing sensitive information stored in your notes.
|
||||
|
||||
> [!WARNING]
|
||||
> OpenID and TOTP cannot be both used at the same time!
|
||||
|
||||
## Log in with your Google Account with OpenID!
|
||||
|
||||
OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity.
|
||||
|
||||
## Why Time-based One Time Passwords?
|
||||
|
||||
TOTP (Time-Based One-Time Password) is a security feature that generates a unique, temporary code on your device, like a smartphone, which changes every 30 seconds. You use this code, along with your password, to log into your account, making it much harder for anyone else to access them.
|
||||
|
||||
## Setup
|
||||
|
||||
MFA can only be set up on a server instance.
|
||||
|
||||
> [!NOTE]
|
||||
> When Multi-Factor Authentication (MFA) is enabled on a server instance, a new desktop instance may fail to sync with it. As a temporary workaround, you can disable MFA to complete the initial sync, then re-enable MFA afterward. This issue will be addressed in a future release.
|
||||
|
||||
### TOTP
|
||||
|
||||
1. Go to "Menu" -> "Options" -> "MFA"
|
||||
2. Click the “Enable Multi-Factor Authentication” checkbox if not checked
|
||||
3. Choose “Time-Based One-Time Password (TOTP)” under MFA Method
|
||||
4. Click the "Generate TOTP Secret" button
|
||||
5. Copy the generated secret to your authentication app/extension
|
||||
6. Click the "Generate Recovery Codes" button
|
||||
7. Save the recovery codes. Recovery codes can be used once in place of the TOTP if you loose access to your authenticator. After a rerecovery code is used, it will show the unix timestamp when it was used in the MFA options tab.
|
||||
8. Re-login will be required after TOTP setup is finished (After you refreshing the page).
|
||||
|
||||
### OpenID
|
||||
|
||||
In order to setup OpenID, you will need to setup a authentication provider. This requires a bit of extra setup. Follow [these instructions](https://developers.google.com/identity/openid-connect/openid-connect) to setup an OpenID service through google. The Redirect URL of Trilium is `https://<your-trilium-domain>/callback`.
|
||||
|
||||
1. Set the `oauthBaseUrl`, `oauthClientId` and `oauthClientSecret` in the `config.ini` file (check <a class="reference-link" href="#root/SneMubD5wTR6">Configuration (config.ini or environment variables)</a> for more information).
|
||||
1. You can also setup through environment variables:
|
||||
* Standard: `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET`
|
||||
* Legacy (still supported): `TRILIUM_OAUTH_BASE_URL`, `TRILIUM_OAUTH_CLIENT_ID`, `TRILIUM_OAUTH_CLIENT_SECRET`
|
||||
2. `oauthBaseUrl` should be the link of your Trilium instance server, for example, `https://<your-trilium-domain>`.
|
||||
2. Restart the server
|
||||
3. Go to "Menu" -> "Options" -> "MFA"
|
||||
4. Click the “Enable Multi-Factor Authentication” checkbox if not checked
|
||||
5. Choose “OAuth/OpenID” under MFA Method
|
||||
6. Refresh the page and login through OpenID provider
|
||||
|
||||
> [!NOTE]
|
||||
> The default OAuth issuer is Google. To use other services such as Authentik or Auth0, you can configure the settings via `oauthIssuerBaseUrl`, `oauthIssuerName`, and `oauthIssuerIcon` in the `config.ini` file. Alternatively, these values can be set using environment variables:
|
||||
>
|
||||
> * Standard: `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME`, `TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON`
|
||||
> * Legacy (still supported): `TRILIUM_OAUTH_ISSUER_BASE_URL`, `TRILIUM_OAUTH_ISSUER_NAME`, `TRILIUM_OAUTH_ISSUER_ICON`
|
||||
>
|
||||
> `oauthIssuerName` and `oauthIssuerIcon` are required for displaying correct issuer information at the Login page.
|
||||
|
||||
#### Authentik
|
||||
|
||||
If you don’t already have a running Authentik instance, please follow [these instructions](https://docs.goauthentik.io/docs/install-config/install/docker-compose) to set one up.
|
||||
|
||||
1. In the Authentik admin dashboard, create a new OAuth2 application by following [these steps](https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/create-oauth2-provider). Make sure to set the Redirect URL to: `https://<your-trilium-domain>/callback`.
|
||||
2. In your config.ini file, set the relevant OAuth variables:
|
||||
1. `oauthIssuerBaseUrl` → Use the `OpenID Configuration Issuer` URL from your application's overview page.
|
||||
2. `oauthIssuerName` and `oauthIssuerIcon` → Set these to customize the name and icon displayed on the login page. If omitted, Google’s name and icon will be shown by default.
|
||||
3. Apply the changes by restarting your server.
|
||||
4. Proceed with the remaining steps starting from Step 3 in the OpenID section.
|
||||
53
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/TLS Configuration.md
vendored
Normal file
53
docs/User Guide/User Guide/Installation & Setup/1_Server Installation/TLS Configuration.md
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# TLS Configuration
|
||||
Configuring TLS is essential for [server installation](../1_Server%20Installation.md) in Trilium. This guide details the steps to set up TLS within Trilium itself.
|
||||
|
||||
For a more robust solution, consider using TLS termination with a reverse proxy (recommended, e.g., Nginx). You can follow a [guide like this](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-20-04) for such setups.
|
||||
|
||||
## Obtaining a TLS Certificate
|
||||
|
||||
You have two options for obtaining a TLS certificate:
|
||||
|
||||
* **Recommended**: Obtain a TLS certificate signed by a root certificate authority. For personal use, [Let's Encrypt](https://letsencrypt.org) is an excellent choice. It is free, automated, and straightforward. Certbot can facilitate automatic TLS setup.
|
||||
* Generate a self-signed certificate. This option is not recommended due to the additional complexity of importing the certificate into all machines connecting to the server.
|
||||
|
||||
## Modifying `config.ini`
|
||||
|
||||
Once you have your certificate, modify the `config.ini` file in the [data directory](#root/dvbMBRXYMM2G) to configure Trilium to use it:
|
||||
|
||||
```
|
||||
[Network]
|
||||
port=8080
|
||||
# Set to true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
|
||||
https=true
|
||||
# Path to the certificate (run "bash bin/generate-cert.sh" to generate a self-signed certificate).
|
||||
# Relevant only if https=true
|
||||
certPath=/[username]/.acme.sh/[hostname]/fullchain.cer
|
||||
keyPath=/[username]/.acme.sh/[hostname]/example.com.key
|
||||
```
|
||||
|
||||
You can also review the [configuration](#root/SneMubD5wTR6) file to provide all `config.ini` values as environment variables instead. For example, you can configure TLS using environment variables:
|
||||
|
||||
```sh
|
||||
export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem
|
||||
```
|
||||
|
||||
The above example shows how this is set up in an environment where the certificate was generated using Let's Encrypt's ACME utility. Your paths may differ. For Docker installations, ensure these paths are within a volume or another directory accessible by the Docker container, such as `/home/node/trilium-data/[DIR IN DATA DIRECTORY]`.
|
||||
|
||||
After configuring `config.ini`, restart Trilium and access the hostname using "https".
|
||||
|
||||
## Self-Signed Certificate
|
||||
|
||||
If you opt to use a self-signed certificate for your server instance, note that the desktop instance will not trust it by default.
|
||||
|
||||
To bypass this, disable certificate validation by setting the following environment variable (for Linux):
|
||||
|
||||
```
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
trilium
|
||||
```
|
||||
|
||||
Trilium provides scripts to start in this mode, such as `trilium-no-cert-check.bat` for Windows.
|
||||
|
||||
**Warning**: Disabling TLS certificate validation is insecure. Proceed only if you fully understand the implications.
|
||||
279
docs/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.md
vendored
Normal file
279
docs/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.md
vendored
Normal file
@@ -0,0 +1,279 @@
|
||||
# Protected Notes and Encryption
|
||||
Trilium provides robust encryption capabilities through its Protected Notes system, ensuring your sensitive information remains secure even if your database is compromised.
|
||||
|
||||
## Overview
|
||||
|
||||
Protected notes in Trilium use **AES-128-CBC encryption** with scrypt-based key derivation to protect sensitive content. The encryption is designed to be:
|
||||
|
||||
* **Secure**: Uses industry-standard AES encryption with strong key derivation
|
||||
* **Selective**: Only notes marked as protected are encrypted
|
||||
* **Session-based**: Decrypted content remains accessible during a protected session
|
||||
* **Zero-knowledge**: The server never stores unencrypted protected content
|
||||
|
||||
## How Encryption Works
|
||||
|
||||
### Encryption Algorithm
|
||||
|
||||
* **Cipher**: AES-128-CBC (Advanced Encryption Standard in Cipher Block Chaining mode)
|
||||
* **Key Derivation**: Scrypt with configurable parameters (N=16384, r=8, p=1)
|
||||
* **Initialization Vector**: 16-byte random IV generated for each encryption operation
|
||||
* **Integrity Protection**: SHA-1 digest (first 4 bytes) prepended to plaintext for tamper detection
|
||||
|
||||
### Key Management
|
||||
|
||||
1. **Master Password**: User-provided password used for key derivation
|
||||
2. **Data Key**: 32-byte random key generated during setup, encrypted with password-derived key
|
||||
3. **Password-Derived Key**: Generated using scrypt from master password and salt
|
||||
4. **Session Key**: Data key loaded into memory during protected session
|
||||
|
||||
### Encryption Process
|
||||
|
||||
```
|
||||
1. Generate random 16-byte IV
|
||||
2. Compute SHA-1 digest of plaintext (use first 4 bytes)
|
||||
3. Prepend digest to plaintext
|
||||
4. Encrypt (digest + plaintext) using AES-128-CBC
|
||||
5. Prepend IV to encrypted data
|
||||
6. Encode result as Base64
|
||||
```
|
||||
|
||||
### Decryption Process
|
||||
|
||||
```
|
||||
1. Decode Base64 ciphertext
|
||||
2. Extract IV (first 16 bytes) and encrypted data
|
||||
3. Decrypt using AES-128-CBC with data key and IV
|
||||
4. Extract digest (first 4 bytes) and plaintext
|
||||
5. Verify integrity by comparing computed vs. stored digest
|
||||
6. Return plaintext if verification succeeds
|
||||
```
|
||||
|
||||
## Setting Up Protected Notes
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Set Master Password**: Configure a strong password during initial setup
|
||||
2. **Create Protected Note**: Right-click a note and select "Toggle Protected Status"
|
||||
3. **Enter Protected Session**: Click the shield icon or use Ctrl+Shift+P
|
||||
|
||||
### Password Requirements
|
||||
|
||||
* **Minimum Length**: 8 characters (recommended: 12+ characters)
|
||||
* **Complexity**: Use a mix of uppercase, lowercase, numbers, and symbols
|
||||
* **Uniqueness**: Don't reuse passwords from other services
|
||||
* **Storage**: Consider using a password manager for complex passwords
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Strong Passwords**: Use passphrases or generated passwords
|
||||
2. **Regular Changes**: Update passwords periodically
|
||||
3. **Secure Storage**: Store password recovery information securely
|
||||
4. **Backup Strategy**: Ensure encrypted backups are properly secured
|
||||
|
||||
## Protected Sessions
|
||||
|
||||
### Session Management
|
||||
|
||||
* **Automatic Timeout**: Sessions expire after configurable timeout (default: 10 minutes)
|
||||
* **Manual Control**: Explicitly enter/exit protected sessions
|
||||
* **Activity Tracking**: Session timeout resets with each protected note access
|
||||
* **Multi-client**: Each client maintains its own protected session
|
||||
|
||||
### Session Lifecycle
|
||||
|
||||
1. **Enter Session**: User enters master password
|
||||
2. **Key Derivation**: System derives data key from password
|
||||
3. **Session Active**: Protected content accessible in plaintext
|
||||
4. **Timeout/Logout**: Data key removed from memory
|
||||
5. **Protection Restored**: Content returns to encrypted state
|
||||
|
||||
### Configuration Options
|
||||
|
||||
Access via Options → Protected Session:
|
||||
|
||||
* **Session Timeout**: Duration before automatic logout (seconds)
|
||||
* **Password Verification**: Enable/disable password strength requirements
|
||||
* **Recovery Options**: Configure password recovery mechanisms
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Encryption Overhead
|
||||
|
||||
* **CPU Impact**: Scrypt key derivation is intentionally CPU-intensive
|
||||
* **Memory Usage**: Minimal additional memory for encrypted content
|
||||
* **Storage Size**: Encrypted content is slightly larger due to Base64 encoding
|
||||
* **Network Transfer**: Encrypted notes transfer as Base64 strings
|
||||
|
||||
### Optimization Tips
|
||||
|
||||
1. **Selective Protection**: Only encrypt truly sensitive notes
|
||||
2. **Session Management**: Keep sessions active during intensive work
|
||||
3. **Hardware Acceleration**: Modern CPUs provide AES acceleration
|
||||
4. **Batch Operations**: Group protected note operations when possible
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
**Protected Against**:
|
||||
|
||||
* Database theft or unauthorized access
|
||||
* Network interception (data at rest)
|
||||
* Server-side data breaches
|
||||
* Backup file compromise
|
||||
|
||||
**Not Protected Against**:
|
||||
|
||||
* Keyloggers or screen capture malware
|
||||
* Physical access to unlocked device
|
||||
* Memory dumps during active session
|
||||
* Social engineering attacks
|
||||
|
||||
### Limitations
|
||||
|
||||
1. **Note Titles**: Currently encrypted, may leak structural information
|
||||
2. **Metadata**: Creation dates, modification times remain unencrypted
|
||||
3. **Search Indexing**: Protected notes excluded from full-text search
|
||||
4. **Sync Conflicts**: May be harder to resolve for protected content
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "Could not decrypt string" Error
|
||||
|
||||
**Causes**:
|
||||
|
||||
* Incorrect password entered
|
||||
* Corrupted encrypted data
|
||||
* Database migration issues
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Verify password spelling and case sensitivity
|
||||
2. Check for active protected session
|
||||
3. Restart application and retry
|
||||
4. Restore from backup if corruption suspected
|
||||
|
||||
#### Protected Session Won't Start
|
||||
|
||||
**Causes**:
|
||||
|
||||
* Password verification hash mismatch
|
||||
* Missing encryption salt
|
||||
* Database schema issues
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Check error logs for specific error messages
|
||||
2. Verify database integrity
|
||||
3. Restore from known good backup
|
||||
4. Contact support with error details
|
||||
|
||||
#### Performance Issues
|
||||
|
||||
**Symptoms**:
|
||||
|
||||
* Slow password verification
|
||||
* Long delays entering protected session
|
||||
* High CPU usage during encryption
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. Reduce scrypt parameters (advanced users only)
|
||||
2. Limit number of protected notes
|
||||
3. Upgrade hardware (more RAM/faster CPU)
|
||||
4. Close other resource-intensive applications
|
||||
|
||||
### Recovery Procedures
|
||||
|
||||
#### Password Recovery
|
||||
|
||||
If you forget your master password:
|
||||
|
||||
1. **No Built-in Recovery**: Trilium cannot recover forgotten passwords
|
||||
2. **Backup Restoration**: Restore from backup with known password
|
||||
3. **Data Export**: Export unprotected content before password change
|
||||
4. **Complete Reset**: Last resort - lose all protected content
|
||||
|
||||
#### Data Recovery
|
||||
|
||||
For corrupted protected notes:
|
||||
|
||||
1. **Verify Backup**: Check if backups contain uncorrupted data
|
||||
2. **Export/Import**: Try exporting and re-importing the note
|
||||
3. **Database Repair**: Use database repair tools if available
|
||||
4. **Professional Help**: Contact data recovery services for critical data
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Encryption Parameters
|
||||
|
||||
**Warning**: Modifying encryption parameters requires advanced knowledge and may break compatibility.
|
||||
|
||||
For expert users, encryption parameters can be modified in the source code:
|
||||
|
||||
```typescript
|
||||
// In my_scrypt.ts
|
||||
const scryptParams = {
|
||||
N: 16384, // CPU/memory cost parameter
|
||||
r: 8, // Block size parameter
|
||||
p: 1 // Parallelization parameter
|
||||
};
|
||||
```
|
||||
|
||||
### Integration with External Tools
|
||||
|
||||
Protected notes can be accessed programmatically:
|
||||
|
||||
```javascript
|
||||
// Backend script example
|
||||
const protectedNote = api.getNote('noteId');
|
||||
if (protectedNote.isProtected) {
|
||||
// Content will be encrypted unless in protected session
|
||||
const content = protectedNote.getContent();
|
||||
}
|
||||
```
|
||||
|
||||
## Compliance and Auditing
|
||||
|
||||
### Encryption Standards
|
||||
|
||||
* **Algorithm**: AES-128-CBC (FIPS 140-2 approved)
|
||||
* **Key Derivation**: Scrypt (RFC 7914)
|
||||
* **Random Generation**: Node.js crypto.randomBytes() (OS entropy)
|
||||
|
||||
### Audit Trail
|
||||
|
||||
* Protected session entry/exit events logged
|
||||
* Encryption/decryption operations tracked
|
||||
* Password verification attempts recorded
|
||||
* Key derivation operations monitored
|
||||
|
||||
### Compliance Considerations
|
||||
|
||||
* **GDPR**: Encryption provides data protection safeguards
|
||||
* **HIPAA**: AES encryption meets security requirements
|
||||
* **SOX**: Audit trails support compliance requirements
|
||||
* **PCI DSS**: Strong encryption protects sensitive data
|
||||
|
||||
## Migration and Backup
|
||||
|
||||
### Backup Strategies
|
||||
|
||||
1. **Encrypted Backups**: Regular backups preserve encrypted state
|
||||
2. **Unencrypted Exports**: Export protected content during session
|
||||
3. **Key Management**: Securely store password recovery information
|
||||
4. **Testing**: Regularly test backup restoration procedures
|
||||
|
||||
### Migration Procedures
|
||||
|
||||
When moving to new installation:
|
||||
|
||||
1. **Export Data**: Export all notes including protected content
|
||||
2. **Backup Database**: Create complete database backup
|
||||
3. **Transfer Files**: Move exported files to new installation
|
||||
4. **Import Data**: Import using same master password
|
||||
5. **Verify**: Confirm all protected content accessible
|
||||
|
||||
Remember: The security of protected notes ultimately depends on choosing a strong master password and following security best practices for your overall system.
|
||||
Reference in New Issue
Block a user