apply new query parsing to note autocomplete

This commit is contained in:
zadam 2020-05-21 00:39:17 +02:00
parent b26100479d
commit 32dde426fd
7 changed files with 84 additions and 69 deletions

View File

@ -19,7 +19,7 @@ class Attribute {
this.noteCache.notes[this.noteId].ownedAttributes.push(this);
const key = `${this.type-this.name}`;
const key = `${this.type}-${this.name}`;
this.noteCache.attributeIndex[key] = this.noteCache.attributeIndex[key] || [];
this.noteCache.attributeIndex[key].push(this);

View File

@ -28,6 +28,8 @@ class AttributeExistsExp {
}
}
}
return resultNoteSet;
}
}

View File

@ -29,6 +29,8 @@ class FieldComparisonExp {
}
}
}
return resultNoteSet;
}
}

View File

@ -11,77 +11,9 @@ class NoteCacheFulltextExp {
execute(noteSet, searchContext) {
// has deps on SQL which breaks unit test so needs to be dynamically required
const noteCacheService = require('../../note_cache/note_cache_service');
const resultNoteSet = new NoteSet();
const candidateNotes = this.getCandidateNotes(noteSet);
for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs)
if (this.tokens.length === 1 && note.noteId === this.tokens[0]) {
this.searchDownThePath(note, [], [], resultNoteSet, searchContext);
continue;
}
// for leaf note it doesn't matter if "archived" label is inheritable or not
if (note.isArchived) {
continue;
}
const foundAttrTokens = [];
for (const attribute of note.ownedAttributes) {
for (const token of this.tokens) {
if (attribute.name.toLowerCase().includes(token)
|| attribute.value.toLowerCase().includes(token)) {
foundAttrTokens.push(token);
}
}
}
for (const parentNote of note.parents) {
const title = noteCacheService.getNoteTitle(note.noteId, parentNote.noteId).toLowerCase();
const foundTokens = foundAttrTokens.slice();
for (const token of this.tokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
}
if (foundTokens.length > 0) {
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
this.searchDownThePath(parentNote, remainingTokens, [note.noteId], resultNoteSet, searchContext);
}
}
}
return resultNoteSet;
}
/**
* Returns noteIds which have at least one matching tokens
*
* @param {NoteSet} noteSet
* @return {String[]}
*/
getCandidateNotes(noteSet) {
const candidateNotes = [];
for (const note of noteSet.notes) {
for (const token of this.tokens) {
if (note.flatText.includes(token)) {
candidateNotes.push(note);
break;
}
}
}
return candidateNotes;
}
searchDownThePath(note, tokens, path, resultNoteSet, searchContext) {
function searchDownThePath(note, tokens, path) {
if (tokens.length === 0) {
const retPath = noteCacheService.getSomePath(note, path);
@ -123,13 +55,80 @@ class NoteCacheFulltextExp {
if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
this.searchDownThePath(parentNote, remainingTokens, path.concat([note.noteId]), resultNoteSet, searchContext);
searchDownThePath(parentNote, remainingTokens, path.concat([note.noteId]));
}
else {
this.searchDownThePath(parentNote, tokens, path.concat([note.noteId]), resultNoteSet, searchContext);
}
searchDownThePath(parentNote, tokens, path.concat([note.noteId]));
}
}
}
const candidateNotes = this.getCandidateNotes(noteSet);
for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs)
if (this.tokens.length === 1 && note.noteId === this.tokens[0]) {
searchDownThePath(note, [], []);
continue;
}
// for leaf note it doesn't matter if "archived" label is inheritable or not
if (note.isArchived) {
continue;
}
const foundAttrTokens = [];
for (const attribute of note.ownedAttributes) {
for (const token of this.tokens) {
if (attribute.name.toLowerCase().includes(token)
|| attribute.value.toLowerCase().includes(token)) {
foundAttrTokens.push(token);
}
}
}
for (const parentNote of note.parents) {
const title = noteCacheService.getNoteTitle(note.noteId, parentNote.noteId).toLowerCase();
const foundTokens = foundAttrTokens.slice();
for (const token of this.tokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
}
if (foundTokens.length > 0) {
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [note.noteId]);
}
}
}
return resultNoteSet;
}
/**
* Returns noteIds which have at least one matching tokens
*
* @param {NoteSet} noteSet
* @return {String[]}
*/
getCandidateNotes(noteSet) {
const candidateNotes = [];
for (const note of noteSet.notes) {
for (const token of this.tokens) {
if (note.flatText.includes(token)) {
candidateNotes.push(note);
break;
}
}
}
return candidateNotes;
}
}
module.exports = NoteCacheFulltextExp;

View File

@ -20,15 +20,13 @@ class NoteContentFulltextExp {
JOIN note_contents ON notes.noteId = note_contents.noteId
WHERE isDeleted = 0 AND isProtected = 0 AND ${wheres.join(' AND ')}`);
const results = [];
for (const noteId of noteIds) {
if (noteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
resultNoteSet.add(noteCache.notes[noteId]);
}
}
return results;
return resultNoteSet;
}
}

View File

@ -3,7 +3,7 @@
*/
function parens(tokens) {
if (tokens.length === 0) {
throw new Error("Empty expression.");
return [];
}
while (true) {

View File

@ -1,14 +1,16 @@
"use strict";
const NoteCacheFulltextExp = require("./expressions/note_cache_fulltext");
const lexer = require('./lexer');
const parens = require('./parens');
const parser = require('./parser');
const NoteSet = require("./note_set");
const SearchResult = require("./search_result");
const noteCache = require('../note_cache/note_cache');
const noteCacheService = require('../note_cache/note_cache_service');
const hoistedNoteService = require('../hoisted_note');
const utils = require('../utils');
async function findNotesWithExpression(expression) {
const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()];
const allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
? hoistedNote.subtreeNotes
@ -23,7 +25,7 @@ async function findNotesWithExpression(expression) {
const noteSet = await expression.execute(allNoteSet, searchContext);
let searchResults = noteSet.notes
.map(note => searchContext.noteIdToNotePath[note.noteId] || getSomePath(note))
.map(note => searchContext.noteIdToNotePath[note.noteId] || noteCacheService.getSomePath(note))
.filter(notePathArray => notePathArray.includes(hoistedNoteService.getHoistedNoteId()))
.map(notePathArray => new SearchResult(notePathArray));
@ -40,24 +42,30 @@ async function findNotesWithExpression(expression) {
return searchResults;
}
function parseQueryToExpression(query) {
const {fulltextTokens, expressionTokens} = lexer(query);
const structuredExpressionTokens = parens(expressionTokens);
const expression = parser(fulltextTokens, structuredExpressionTokens, false);
return expression;
}
async function searchNotesForAutocomplete(query) {
if (!query.trim().length) {
return [];
}
const tokens = query
.trim() // necessary because even with .split() trailing spaces are tokens which causes havoc
.toLowerCase()
.split(/[ -]/)
.filter(token => token !== '/'); // '/' is used as separator
const expression = parseQueryToExpression(query);
const expression = new NoteCacheFulltextExp(tokens);
if (!expression) {
return [];
}
let searchResults = await findNotesWithExpression(expression);
searchResults = searchResults.slice(0, 200);
highlightSearchResults(searchResults, tokens);
highlightSearchResults(searchResults, query);
return searchResults.map(result => {
return {
@ -68,7 +76,13 @@ async function searchNotesForAutocomplete(query) {
});
}
function highlightSearchResults(searchResults, tokens) {
function highlightSearchResults(searchResults, query) {
let tokens = query
.trim() // necessary because even with .split() trailing spaces are tokens which causes havoc
.toLowerCase()
.split(/[ -]/)
.filter(token => token !== '/');
// 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)