mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 18:05:55 +01:00 
			
		
		
		
	note cache refactoring WIP
This commit is contained in:
		| @@ -75,7 +75,7 @@ function updateTitleFormGroupVisibility() { | ||||
| } | ||||
|  | ||||
| $form.on('submit', () => { | ||||
|     const notePath = $autoComplete.getSelectedPath(); | ||||
|     const notePath = $autoComplete.getSelectedNotePath(); | ||||
|  | ||||
|     if (notePath) { | ||||
|         $dialog.modal('hide'); | ||||
|   | ||||
| @@ -269,7 +269,7 @@ function initKoPlugins() { | ||||
|         init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { | ||||
|             noteAutocompleteService.initNoteAutocomplete($(element)); | ||||
|  | ||||
|             $(element).setSelectedPath(bindingContext.$data.selectedPath); | ||||
|             $(element).setSelectedNotePath(bindingContext.$data.selectedPath); | ||||
|  | ||||
|             $(element).on('autocomplete:selected', function (event, suggestion, dataset) { | ||||
|                 bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : ''; | ||||
|   | ||||
| @@ -52,7 +52,7 @@ async function cloneNotesTo(notePath) { | ||||
| } | ||||
|  | ||||
| $form.on('submit', () => { | ||||
|     const notePath = $noteAutoComplete.getSelectedPath(); | ||||
|     const notePath = $noteAutoComplete.getSelectedNotePath(); | ||||
|  | ||||
|     if (notePath) { | ||||
|         $dialog.modal('hide'); | ||||
|   | ||||
| @@ -38,7 +38,7 @@ async function includeNote(notePath) { | ||||
| } | ||||
|  | ||||
| $form.on('submit', () => { | ||||
|     const notePath = $autoComplete.getSelectedPath(); | ||||
|     const notePath = $autoComplete.getSelectedNotePath(); | ||||
|  | ||||
|     if (notePath) { | ||||
|         $dialog.modal('hide'); | ||||
|   | ||||
| @@ -41,7 +41,7 @@ async function moveNotesTo(parentNoteId) { | ||||
| } | ||||
|  | ||||
| $form.on('submit', () => { | ||||
|     const notePath = $noteAutoComplete.getSelectedPath(); | ||||
|     const notePath = $noteAutoComplete.getSelectedNotePath(); | ||||
|  | ||||
|     if (notePath) { | ||||
|         $dialog.modal('hide'); | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import appContext from "./app_context.js"; | ||||
| import utils from './utils.js'; | ||||
|  | ||||
| // this key needs to have this value so it's hit by the tooltip | ||||
| const SELECTED_PATH_KEY = "data-note-path"; | ||||
| const SELECTED_NOTE_PATH_KEY = "data-note-path"; | ||||
|  | ||||
| async function autocompleteSource(term, cb) { | ||||
|     const result = await server.get('autocomplete' | ||||
| @@ -12,8 +12,8 @@ async function autocompleteSource(term, cb) { | ||||
|  | ||||
|     if (result.length === 0) { | ||||
|         result.push({ | ||||
|             pathTitle: "No results", | ||||
|             path: "" | ||||
|             notePathTitle: "No results", | ||||
|             notePath: "" | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -25,7 +25,7 @@ function clearText($el) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     $el.setSelectedPath(""); | ||||
|     $el.setSelectedNotePath(""); | ||||
|     $el.autocomplete("val", "").trigger('change'); | ||||
| } | ||||
|  | ||||
| @@ -34,7 +34,7 @@ function showRecentNotes($el) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     $el.setSelectedPath(""); | ||||
|     $el.setSelectedNotePath(""); | ||||
|     $el.autocomplete("val", ""); | ||||
|     $el.trigger('focus'); | ||||
| } | ||||
| @@ -91,10 +91,10 @@ function initNoteAutocomplete($el, options) { | ||||
|     }, [ | ||||
|         { | ||||
|             source: autocompleteSource, | ||||
|             displayKey: 'pathTitle', | ||||
|             displayKey: 'notePathTitle', | ||||
|             templates: { | ||||
|                 suggestion: function(suggestion) { | ||||
|                     return suggestion.highlightedTitle; | ||||
|                     return suggestion.highlightedNotePathTitle; | ||||
|                 } | ||||
|             }, | ||||
|             // we can't cache identical searches because notes can be created / renamed, new recent notes can be added | ||||
| @@ -102,7 +102,7 @@ function initNoteAutocomplete($el, options) { | ||||
|         } | ||||
|     ]); | ||||
|  | ||||
|     $el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedPath(suggestion.path)); | ||||
|     $el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedNotePath(suggestion.notePath)); | ||||
|     $el.on('autocomplete:closed', () => { | ||||
|         if (!$el.val().trim()) { | ||||
|             clearText($el); | ||||
| @@ -113,24 +113,24 @@ function initNoteAutocomplete($el, options) { | ||||
| } | ||||
|  | ||||
| function init() { | ||||
|     $.fn.getSelectedPath = function () { | ||||
|     $.fn.getSelectedNotePath = function () { | ||||
|         if (!$(this).val().trim()) { | ||||
|             return ""; | ||||
|         } else { | ||||
|             return $(this).attr(SELECTED_PATH_KEY); | ||||
|             return $(this).attr(SELECTED_NOTE_PATH_KEY); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     $.fn.setSelectedPath = function (path) { | ||||
|         path = path || ""; | ||||
|     $.fn.setSelectedNotePath = function (notePath) { | ||||
|         notePath = notePath || ""; | ||||
|  | ||||
|         $(this).attr(SELECTED_PATH_KEY, path); | ||||
|         $(this).attr(SELECTED_NOTE_PATH_KEY, notePath); | ||||
|  | ||||
|         $(this) | ||||
|             .closest(".input-group") | ||||
|             .find(".go-to-selected-note-button") | ||||
|             .toggleClass("disabled", !path.trim()) | ||||
|             .attr(SELECTED_PATH_KEY, path); // we also set attr here so tooltip can be displayed | ||||
|             .toggleClass("disabled", !notePath.trim()) | ||||
|             .attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed | ||||
|     }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -200,7 +200,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget { | ||||
|                 this.promotedAttributeChanged(event); | ||||
|             }); | ||||
|  | ||||
|             $input.setSelectedPath(valueAttr.value); | ||||
|             $input.setSelectedNotePath(valueAttr.value); | ||||
|         } | ||||
|         else { | ||||
|             ws.logError("Unknown attribute type=" + valueAttr.type); | ||||
| @@ -250,7 +250,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget { | ||||
|             value = $attr.is(':checked') ? "true" : "false"; | ||||
|         } | ||||
|         else if ($attr.prop("attribute-type") === "relation") { | ||||
|             const selectedPath = $attr.getSelectedPath(); | ||||
|             const selectedPath = $attr.getSelectedNotePath(); | ||||
|  | ||||
|             value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : ""; | ||||
|         } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ async function getAutocomplete(req) { | ||||
|         results = await getRecentNotes(activeNoteId); | ||||
|     } | ||||
|     else { | ||||
|         results = await noteCacheService.findNotesWithFulltext(query); | ||||
|         results = await noteCacheService.findNotesForAutocomplete(query); | ||||
|     } | ||||
|  | ||||
|     const msTaken = Date.now() - timestampStarted; | ||||
| @@ -57,10 +57,9 @@ async function getRecentNotes(activeNoteId) { | ||||
|         const title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/')); | ||||
|  | ||||
|         return { | ||||
|             path: rn.notePath, | ||||
|             pathTitle: title, | ||||
|             highlightedTitle: title, | ||||
|             noteTitle: noteCacheService.getNoteTitleFromPath(rn.notePath) | ||||
|             notePath: rn.notePath, | ||||
|             notePathTitle: title, | ||||
|             highlightedNotePathTitle: utils.escapeHtml(title) | ||||
|         }; | ||||
|     }); | ||||
| } | ||||
|   | ||||
| @@ -371,6 +371,8 @@ async function load() { | ||||
|     branches = await getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`, | ||||
|         row => new Branch(row)); | ||||
|  | ||||
|     attributeIndex = []; | ||||
|  | ||||
|     attributes = await getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`, | ||||
|         row => new Attribute(row)); | ||||
|  | ||||
| @@ -378,17 +380,7 @@ async function load() { | ||||
|     loadedPromiseResolve(); | ||||
| } | ||||
|  | ||||
| const expression = { | ||||
|     operator: 'and', | ||||
|     operands: [ | ||||
|         { | ||||
|             operator: 'exists', | ||||
|             fieldName: 'hokus' | ||||
|         } | ||||
|     ] | ||||
| }; | ||||
|  | ||||
| class AndOp { | ||||
| class AndExp { | ||||
|     constructor(subExpressions) { | ||||
|         this.subExpressions = subExpressions; | ||||
|     } | ||||
| @@ -402,7 +394,7 @@ class AndOp { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class OrOp { | ||||
| class OrExp { | ||||
|     constructor(subExpressions) { | ||||
|         this.subExpressions = subExpressions; | ||||
|     } | ||||
| @@ -441,7 +433,7 @@ class NoteSet { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class ExistsOp { | ||||
| class ExistsExp { | ||||
|     constructor(attributeType, attributeName) { | ||||
|         this.attributeType = attributeType; | ||||
|         this.attributeName = attributeName; | ||||
| @@ -469,7 +461,7 @@ class ExistsOp { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class EqualsOp { | ||||
| class EqualsExp { | ||||
|     constructor(attributeType, attributeName, attributeValue) { | ||||
|         this.attributeType = attributeType; | ||||
|         this.attributeName = attributeName; | ||||
| @@ -498,7 +490,7 @@ class EqualsOp { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class NoteContentFulltextOp { | ||||
| class NoteContentFulltextExp { | ||||
|     constructor(tokens) { | ||||
|         this.tokens = tokens; | ||||
|     } | ||||
| @@ -525,7 +517,7 @@ class NoteContentFulltextOp { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class NoteCacheFulltextOp { | ||||
| class NoteCacheFulltextExp { | ||||
|     constructor(tokens) { | ||||
|         this.tokens = tokens; | ||||
|     } | ||||
| @@ -569,7 +561,7 @@ class NoteCacheFulltextOp { | ||||
|                 } | ||||
|  | ||||
|                 if (foundTokens.length > 0) { | ||||
|                     const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); | ||||
|                     const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token)); | ||||
|  | ||||
|                     this.searchDownThePath(parentNote, remainingTokens, [note.noteId], resultNoteSet, searchContext); | ||||
|                 } | ||||
| @@ -651,6 +643,21 @@ class NoteCacheFulltextOp { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class SearchResult { | ||||
|     constructor(notePathArray) { | ||||
|         this.notePathArray = notePathArray; | ||||
|         this.notePathTitle = getNoteTitleForPath(notePathArray); | ||||
|     } | ||||
|  | ||||
|     get notePath() { | ||||
|         return this.notePathArray.join('/'); | ||||
|     } | ||||
|  | ||||
|     get noteId() { | ||||
|         return this.notePathArray[this.notePathArray.length - 1]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function findNotesWithExpression(expression) { | ||||
|  | ||||
|     const hoistedNote = notes[hoistedNoteService.getHoistedNoteId()]; | ||||
| @@ -664,10 +671,27 @@ async function findNotesWithExpression(expression) { | ||||
|         noteIdToNotePath: {} | ||||
|     }; | ||||
|  | ||||
|     expression.execute(allNoteSet, searchContext); | ||||
|     const noteSet = await expression.execute(allNoteSet, searchContext); | ||||
|  | ||||
|     let searchResults = noteSet.notes | ||||
|         .map(note => searchContext.noteIdToNotePath[note.noteId] || getSomePath(note)) | ||||
|         .filter(notePathArray => notePathArray.includes(hoistedNoteService.getHoistedNoteId())) | ||||
|         .map(notePathArray => new SearchResult(notePathArray)); | ||||
|  | ||||
|     // sort results by depth of the note. This is based on the assumption that more important results | ||||
|     // are closer to the note root. | ||||
|     searchResults.sort((a, b) => { | ||||
|         if (a.notePathArray.length === b.notePathArray.length) { | ||||
|             return a.notePathTitle < b.notePathTitle ? -1 : 1; | ||||
|         } | ||||
|  | ||||
| async function findNotesWithFulltext(query, searchInContent) { | ||||
|         return a.notePathArray.length < b.notePathArray.length ? -1 : 1; | ||||
|     }); | ||||
|  | ||||
|     return searchResults; | ||||
| } | ||||
|  | ||||
| async function findNotesForAutocomplete(query) { | ||||
|     if (!query.trim().length) { | ||||
|         return []; | ||||
|     } | ||||
| @@ -678,74 +702,54 @@ async function findNotesWithFulltext(query, searchInContent) { | ||||
|         .split(/[ -]/) | ||||
|         .filter(token => token !== '/'); // '/' is used as separator | ||||
|  | ||||
|     const cacheResults = findInNoteCache(tokens); | ||||
|     const expression = new NoteCacheFulltextExp(tokens); | ||||
|  | ||||
|     const contentResults = searchInContent ? await findInNoteContent(tokens) : []; | ||||
|     let searchResults = await findNotesWithExpression(expression); | ||||
|  | ||||
|     let results = cacheResults.concat(contentResults); | ||||
|     searchResults = searchResults.slice(0, 200); | ||||
|  | ||||
|     if (hoistedNoteService.getHoistedNoteId() !== 'root') { | ||||
|         results = results.filter(res => res.pathArray.includes(hoistedNoteService.getHoistedNoteId())); | ||||
|     } | ||||
|  | ||||
|     // sort results by depth of the note. This is based on the assumption that more important results | ||||
|     // are closer to the note root. | ||||
|     results.sort((a, b) => { | ||||
|         if (a.pathArray.length === b.pathArray.length) { | ||||
|             return a.title < b.title ? -1 : 1; | ||||
|         } | ||||
|  | ||||
|         return a.pathArray.length < b.pathArray.length ? -1 : 1; | ||||
|     }); | ||||
|  | ||||
|     const apiResults = results.slice(0, 200).map(res => { | ||||
|         const notePath = res.pathArray.join('/'); | ||||
|     highlightSearchResults(searchResults, tokens); | ||||
|  | ||||
|     return searchResults.map(result => { | ||||
|         return { | ||||
|             noteId: res.noteId, | ||||
|             branchId: res.branchId, | ||||
|             path: notePath, | ||||
|             pathTitle: res.titleArray.join(' / '), | ||||
|             noteTitle: getNoteTitleFromPath(notePath) | ||||
|         }; | ||||
|             notePath: result.notePath, | ||||
|             notePathTitle: result.notePathTitle, | ||||
|             highlightedNotePathTitle: result.highlightedNotePathTitle | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     highlightResults(apiResults, tokens); | ||||
|  | ||||
|     return apiResults; | ||||
| } | ||||
|  | ||||
| function highlightResults(results, allTokens) { | ||||
| function highlightSearchResults(searchResults, tokens) { | ||||
|     // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks | ||||
|     // which would make the resulting HTML string invalid. | ||||
|     // { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character) | ||||
|     allTokens = allTokens.map(token => token.replace('/[<\{\}]/g', '')); | ||||
|     tokens = tokens.map(token => token.replace('/[<\{\}]/g', '')); | ||||
|  | ||||
|     // sort by the longest so we first highlight longest matches | ||||
|     allTokens.sort((a, b) => a.length > b.length ? -1 : 1); | ||||
|     tokens.sort((a, b) => a.length > b.length ? -1 : 1); | ||||
|  | ||||
|     for (const result of results) { | ||||
|     for (const result of searchResults) { | ||||
|         const note = notes[result.noteId]; | ||||
|  | ||||
|         result.highlightedNotePathTitle = result.notePathTitle; | ||||
|  | ||||
|         for (const attr of note.attributes) { | ||||
|             if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) { | ||||
|                 result.pathTitle += ` <small>${formatAttribute(attr)}</small>`; | ||||
|             if (tokens.find(token => attr.name.includes(token) || attr.value.includes(token))) { | ||||
|                 result.highlightedNotePathTitle += ` <small>${formatAttribute(attr)}</small>`; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         result.highlightedTitle = result.pathTitle; | ||||
|     } | ||||
|  | ||||
|     for (const token of allTokens) { | ||||
|     for (const token of tokens) { | ||||
|         const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi"); | ||||
|  | ||||
|         for (const result of results) { | ||||
|             result.highlightedTitle = result.highlightedTitle.replace(tokenRegex, "{$1}"); | ||||
|         for (const result of searchResults) { | ||||
|             result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(tokenRegex, "{$1}"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     for (const result of results) { | ||||
|         result.highlightedTitle = result.highlightedTitle | ||||
|     for (const result of searchResults) { | ||||
|         result.highlightedNotePathTitle = result.highlightedNotePathTitle | ||||
|             .replace(/{/g, "<b>") | ||||
|             .replace(/}/g, "</b>"); | ||||
|     } | ||||
| @@ -839,17 +843,6 @@ function isInAncestor(noteId, ancestorNoteId) { | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| function getNoteTitleFromPath(notePath) { | ||||
|     const pathArr = notePath.split("/"); | ||||
|  | ||||
|     if (pathArr.length === 1) { | ||||
|         return getNoteTitle(pathArr[0], 'root'); | ||||
|     } | ||||
|     else { | ||||
|         return getNoteTitle(pathArr[pathArr.length - 1], pathArr[pathArr.length - 2]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getNoteTitle(childNoteId, parentNoteId) { | ||||
|     const childNote = notes[childNoteId]; | ||||
|     const parentNote = notes[parentNoteId]; | ||||
| @@ -868,17 +861,17 @@ function getNoteTitle(childNoteId, parentNoteId) { | ||||
|     return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title; | ||||
| } | ||||
|  | ||||
| function getNoteTitleArrayForPath(path) { | ||||
| function getNoteTitleArrayForPath(notePathArray) { | ||||
|     const titles = []; | ||||
|  | ||||
|     if (path[0] === hoistedNoteService.getHoistedNoteId() && path.length === 1) { | ||||
|     if (notePathArray[0] === hoistedNoteService.getHoistedNoteId() && notePathArray.length === 1) { | ||||
|         return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ]; | ||||
|     } | ||||
|  | ||||
|     let parentNoteId = 'root'; | ||||
|     let hoistedNotePassed = false; | ||||
|  | ||||
|     for (const noteId of path) { | ||||
|     for (const noteId of notePathArray) { | ||||
|         // start collecting path segment titles only after hoisted note | ||||
|         if (hoistedNotePassed) { | ||||
|             const title = getNoteTitle(noteId, parentNoteId); | ||||
| @@ -896,8 +889,8 @@ function getNoteTitleArrayForPath(path) { | ||||
|     return titles; | ||||
| } | ||||
|  | ||||
| function getNoteTitleForPath(path) { | ||||
|     const titles = getNoteTitleArrayForPath(path); | ||||
| function getNoteTitleForPath(notePathArray) { | ||||
|     const titles = getNoteTitleArrayForPath(notePathArray); | ||||
|  | ||||
|     return titles.join(' / '); | ||||
| } | ||||
| @@ -1153,10 +1146,9 @@ sqlInit.dbReady.then(() => utils.stopWatch("Note cache load", load)); | ||||
|  | ||||
| module.exports = { | ||||
|     loadedPromise, | ||||
|     findNotesWithFulltext, | ||||
|     findNotesForAutocomplete, | ||||
|     getNotePath, | ||||
|     getNoteTitleForPath, | ||||
|     getNoteTitleFromPath, | ||||
|     isAvailable, | ||||
|     isArchived, | ||||
|     isInAncestor, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user