mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
note cache refactoring WIP
This commit is contained in:
parent
78ea0b4ba9
commit
e3071e630a
@ -75,7 +75,7 @@ function updateTitleFormGroupVisibility() {
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $autoComplete.getSelectedPath();
|
||||
const notePath = $autoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
@ -89,4 +89,4 @@ $form.on('submit', () => {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
@ -64,4 +64,4 @@ $form.on('submit', () => {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ async function includeNote(notePath) {
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $autoComplete.getSelectedPath();
|
||||
const notePath = $autoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
@ -50,4 +50,4 @@ $form.on('submit', () => {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ async function moveNotesTo(parentNoteId) {
|
||||
}
|
||||
|
||||
$form.on('submit', () => {
|
||||
const notePath = $noteAutoComplete.getSelectedPath();
|
||||
const notePath = $noteAutoComplete.getSelectedNotePath();
|
||||
|
||||
if (notePath) {
|
||||
$dialog.modal('hide');
|
||||
@ -55,4 +55,4 @@ $form.on('submit', () => {
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -139,4 +139,4 @@ export default {
|
||||
initNoteAutocomplete,
|
||||
showRecentNotes,
|
||||
init
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
|
||||
});
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
async function findNotesWithFulltext(query, searchInContent) {
|
||||
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user