note cache refactoring WIP

This commit is contained in:
zadam 2020-05-16 22:11:09 +02:00
parent 78ea0b4ba9
commit e3071e630a
9 changed files with 102 additions and 111 deletions

View File

@ -75,7 +75,7 @@ function updateTitleFormGroupVisibility() {
} }
$form.on('submit', () => { $form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath(); const notePath = $autoComplete.getSelectedNotePath();
if (notePath) { if (notePath) {
$dialog.modal('hide'); $dialog.modal('hide');
@ -89,4 +89,4 @@ $form.on('submit', () => {
} }
return false; return false;
}); });

View File

@ -269,7 +269,7 @@ function initKoPlugins() {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
noteAutocompleteService.initNoteAutocomplete($(element)); noteAutocompleteService.initNoteAutocomplete($(element));
$(element).setSelectedPath(bindingContext.$data.selectedPath); $(element).setSelectedNotePath(bindingContext.$data.selectedPath);
$(element).on('autocomplete:selected', function (event, suggestion, dataset) { $(element).on('autocomplete:selected', function (event, suggestion, dataset) {
bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : ''; bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : '';

View File

@ -52,7 +52,7 @@ async function cloneNotesTo(notePath) {
} }
$form.on('submit', () => { $form.on('submit', () => {
const notePath = $noteAutoComplete.getSelectedPath(); const notePath = $noteAutoComplete.getSelectedNotePath();
if (notePath) { if (notePath) {
$dialog.modal('hide'); $dialog.modal('hide');
@ -64,4 +64,4 @@ $form.on('submit', () => {
} }
return false; return false;
}); });

View File

@ -38,7 +38,7 @@ async function includeNote(notePath) {
} }
$form.on('submit', () => { $form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath(); const notePath = $autoComplete.getSelectedNotePath();
if (notePath) { if (notePath) {
$dialog.modal('hide'); $dialog.modal('hide');
@ -50,4 +50,4 @@ $form.on('submit', () => {
} }
return false; return false;
}); });

View File

@ -41,7 +41,7 @@ async function moveNotesTo(parentNoteId) {
} }
$form.on('submit', () => { $form.on('submit', () => {
const notePath = $noteAutoComplete.getSelectedPath(); const notePath = $noteAutoComplete.getSelectedNotePath();
if (notePath) { if (notePath) {
$dialog.modal('hide'); $dialog.modal('hide');
@ -55,4 +55,4 @@ $form.on('submit', () => {
} }
return false; return false;
}); });

View File

@ -3,7 +3,7 @@ import appContext from "./app_context.js";
import utils from './utils.js'; import utils from './utils.js';
// this key needs to have this value so it's hit by the tooltip // 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) { async function autocompleteSource(term, cb) {
const result = await server.get('autocomplete' const result = await server.get('autocomplete'
@ -12,8 +12,8 @@ async function autocompleteSource(term, cb) {
if (result.length === 0) { if (result.length === 0) {
result.push({ result.push({
pathTitle: "No results", notePathTitle: "No results",
path: "" notePath: ""
}); });
} }
@ -25,7 +25,7 @@ function clearText($el) {
return; return;
} }
$el.setSelectedPath(""); $el.setSelectedNotePath("");
$el.autocomplete("val", "").trigger('change'); $el.autocomplete("val", "").trigger('change');
} }
@ -34,7 +34,7 @@ function showRecentNotes($el) {
return; return;
} }
$el.setSelectedPath(""); $el.setSelectedNotePath("");
$el.autocomplete("val", ""); $el.autocomplete("val", "");
$el.trigger('focus'); $el.trigger('focus');
} }
@ -91,10 +91,10 @@ function initNoteAutocomplete($el, options) {
}, [ }, [
{ {
source: autocompleteSource, source: autocompleteSource,
displayKey: 'pathTitle', displayKey: 'notePathTitle',
templates: { templates: {
suggestion: function(suggestion) { 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 // 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', () => { $el.on('autocomplete:closed', () => {
if (!$el.val().trim()) { if (!$el.val().trim()) {
clearText($el); clearText($el);
@ -113,24 +113,24 @@ function initNoteAutocomplete($el, options) {
} }
function init() { function init() {
$.fn.getSelectedPath = function () { $.fn.getSelectedNotePath = function () {
if (!$(this).val().trim()) { if (!$(this).val().trim()) {
return ""; return "";
} else { } else {
return $(this).attr(SELECTED_PATH_KEY); return $(this).attr(SELECTED_NOTE_PATH_KEY);
} }
}; };
$.fn.setSelectedPath = function (path) { $.fn.setSelectedNotePath = function (notePath) {
path = path || ""; notePath = notePath || "";
$(this).attr(SELECTED_PATH_KEY, path); $(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this) $(this)
.closest(".input-group") .closest(".input-group")
.find(".go-to-selected-note-button") .find(".go-to-selected-note-button")
.toggleClass("disabled", !path.trim()) .toggleClass("disabled", !notePath.trim())
.attr(SELECTED_PATH_KEY, path); // we also set attr here so tooltip can be displayed .attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
}; };
} }
@ -139,4 +139,4 @@ export default {
initNoteAutocomplete, initNoteAutocomplete,
showRecentNotes, showRecentNotes,
init init
} }

View File

@ -200,7 +200,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
this.promotedAttributeChanged(event); this.promotedAttributeChanged(event);
}); });
$input.setSelectedPath(valueAttr.value); $input.setSelectedNotePath(valueAttr.value);
} }
else { else {
ws.logError("Unknown attribute type=" + valueAttr.type); ws.logError("Unknown attribute type=" + valueAttr.type);
@ -250,7 +250,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
value = $attr.is(':checked') ? "true" : "false"; value = $attr.is(':checked') ? "true" : "false";
} }
else if ($attr.prop("attribute-type") === "relation") { else if ($attr.prop("attribute-type") === "relation") {
const selectedPath = $attr.getSelectedPath(); const selectedPath = $attr.getSelectedNotePath();
value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : ""; value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : "";
} }

View File

@ -18,7 +18,7 @@ async function getAutocomplete(req) {
results = await getRecentNotes(activeNoteId); results = await getRecentNotes(activeNoteId);
} }
else { else {
results = await noteCacheService.findNotesWithFulltext(query); results = await noteCacheService.findNotesForAutocomplete(query);
} }
const msTaken = Date.now() - timestampStarted; const msTaken = Date.now() - timestampStarted;
@ -57,10 +57,9 @@ async function getRecentNotes(activeNoteId) {
const title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/')); const title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
return { return {
path: rn.notePath, notePath: rn.notePath,
pathTitle: title, notePathTitle: title,
highlightedTitle: title, highlightedNotePathTitle: utils.escapeHtml(title)
noteTitle: noteCacheService.getNoteTitleFromPath(rn.notePath)
}; };
}); });
} }

View File

@ -371,6 +371,8 @@ async function load() {
branches = await getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`, branches = await getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`,
row => new Branch(row)); row => new Branch(row));
attributeIndex = [];
attributes = await getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`, attributes = await getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`,
row => new Attribute(row)); row => new Attribute(row));
@ -378,17 +380,7 @@ async function load() {
loadedPromiseResolve(); loadedPromiseResolve();
} }
const expression = { class AndExp {
operator: 'and',
operands: [
{
operator: 'exists',
fieldName: 'hokus'
}
]
};
class AndOp {
constructor(subExpressions) { constructor(subExpressions) {
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
@ -402,7 +394,7 @@ class AndOp {
} }
} }
class OrOp { class OrExp {
constructor(subExpressions) { constructor(subExpressions) {
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
@ -441,7 +433,7 @@ class NoteSet {
} }
} }
class ExistsOp { class ExistsExp {
constructor(attributeType, attributeName) { constructor(attributeType, attributeName) {
this.attributeType = attributeType; this.attributeType = attributeType;
this.attributeName = attributeName; this.attributeName = attributeName;
@ -469,7 +461,7 @@ class ExistsOp {
} }
} }
class EqualsOp { class EqualsExp {
constructor(attributeType, attributeName, attributeValue) { constructor(attributeType, attributeName, attributeValue) {
this.attributeType = attributeType; this.attributeType = attributeType;
this.attributeName = attributeName; this.attributeName = attributeName;
@ -498,7 +490,7 @@ class EqualsOp {
} }
} }
class NoteContentFulltextOp { class NoteContentFulltextExp {
constructor(tokens) { constructor(tokens) {
this.tokens = tokens; this.tokens = tokens;
} }
@ -525,7 +517,7 @@ class NoteContentFulltextOp {
} }
} }
class NoteCacheFulltextOp { class NoteCacheFulltextExp {
constructor(tokens) { constructor(tokens) {
this.tokens = tokens; this.tokens = tokens;
} }
@ -569,7 +561,7 @@ class NoteCacheFulltextOp {
} }
if (foundTokens.length > 0) { 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); 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) { async function findNotesWithExpression(expression) {
const hoistedNote = notes[hoistedNoteService.getHoistedNoteId()]; const hoistedNote = notes[hoistedNoteService.getHoistedNoteId()];
@ -664,10 +671,27 @@ async function findNotesWithExpression(expression) {
noteIdToNotePath: {} 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) { if (!query.trim().length) {
return []; return [];
} }
@ -678,74 +702,54 @@ async function findNotesWithFulltext(query, searchInContent) {
.split(/[ -]/) .split(/[ -]/)
.filter(token => token !== '/'); // '/' is used as separator .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') { highlightSearchResults(searchResults, tokens);
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('/');
return searchResults.map(result => {
return { return {
noteId: res.noteId, notePath: result.notePath,
branchId: res.branchId, notePathTitle: result.notePathTitle,
path: notePath, highlightedNotePathTitle: result.highlightedNotePathTitle
pathTitle: res.titleArray.join(' / '), }
noteTitle: getNoteTitleFromPath(notePath)
};
}); });
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 // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
// which would make the resulting HTML string invalid. // which would make the resulting HTML string invalid.
// { and } are used for marking <b> and </b> tag (to avoid matches on single 'b' character) // { 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 // 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]; const note = notes[result.noteId];
result.highlightedNotePathTitle = result.notePathTitle;
for (const attr of note.attributes) { for (const attr of note.attributes) {
if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) { if (tokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
result.pathTitle += ` <small>${formatAttribute(attr)}</small>`; 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"); const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi");
for (const result of results) { for (const result of searchResults) {
result.highlightedTitle = result.highlightedTitle.replace(tokenRegex, "{$1}"); result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(tokenRegex, "{$1}");
} }
} }
for (const result of results) { for (const result of searchResults) {
result.highlightedTitle = result.highlightedTitle result.highlightedNotePathTitle = result.highlightedNotePathTitle
.replace(/{/g, "<b>") .replace(/{/g, "<b>")
.replace(/}/g, "</b>"); .replace(/}/g, "</b>");
} }
@ -839,17 +843,6 @@ function isInAncestor(noteId, ancestorNoteId) {
return false; 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) { function getNoteTitle(childNoteId, parentNoteId) {
const childNote = notes[childNoteId]; const childNote = notes[childNoteId];
const parentNote = notes[parentNoteId]; const parentNote = notes[parentNoteId];
@ -868,17 +861,17 @@ function getNoteTitle(childNoteId, parentNoteId) {
return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title; return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
} }
function getNoteTitleArrayForPath(path) { function getNoteTitleArrayForPath(notePathArray) {
const titles = []; const titles = [];
if (path[0] === hoistedNoteService.getHoistedNoteId() && path.length === 1) { if (notePathArray[0] === hoistedNoteService.getHoistedNoteId() && notePathArray.length === 1) {
return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ]; return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ];
} }
let parentNoteId = 'root'; let parentNoteId = 'root';
let hoistedNotePassed = false; let hoistedNotePassed = false;
for (const noteId of path) { for (const noteId of notePathArray) {
// start collecting path segment titles only after hoisted note // start collecting path segment titles only after hoisted note
if (hoistedNotePassed) { if (hoistedNotePassed) {
const title = getNoteTitle(noteId, parentNoteId); const title = getNoteTitle(noteId, parentNoteId);
@ -896,8 +889,8 @@ function getNoteTitleArrayForPath(path) {
return titles; return titles;
} }
function getNoteTitleForPath(path) { function getNoteTitleForPath(notePathArray) {
const titles = getNoteTitleArrayForPath(path); const titles = getNoteTitleArrayForPath(notePathArray);
return titles.join(' / '); return titles.join(' / ');
} }
@ -1153,10 +1146,9 @@ sqlInit.dbReady.then(() => utils.stopWatch("Note cache load", load));
module.exports = { module.exports = {
loadedPromise, loadedPromise,
findNotesWithFulltext, findNotesForAutocomplete,
getNotePath, getNotePath,
getNoteTitleForPath, getNoteTitleForPath,
getNoteTitleFromPath,
isAvailable, isAvailable,
isArchived, isArchived,
isInAncestor, isInAncestor,