diff --git a/spec/lexer.spec.js b/spec/lexer.spec.js index 14f4314fb..7532faa35 100644 --- a/spec/lexer.spec.js +++ b/spec/lexer.spec.js @@ -18,8 +18,8 @@ describe("Lexer fulltext", () => { }); it("you can use different quotes and other special characters inside quotes", () => { - expect(lexer("'I can use \" or ` or #@=*' without problem").fulltextTokens) - .toEqual(["I can use \" or ` or #@=*", "without", "problem"]); + expect(lexer("'i can use \" or ` or #@=*' without problem").fulltextTokens) + .toEqual(["i can use \" or ` or #@=*", "without", "problem"]); }); it("if quote is not ended then it's just one long token", () => { @@ -56,6 +56,6 @@ describe("Lexer expression", () => { it("complex expressions with and, or and parenthesis", () => { expect(lexer(`# (#label=text OR #second=text) AND @relation`).expressionTokens) - .toEqual(["#", "(", "#label", "=", "text", "OR", "#second", "=", "text", ")", "AND", "@relation"]); + .toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "@relation"]); }); }); diff --git a/spec/search.spec.js b/spec/search.spec.js index 836043da1..907e74e0a 100644 --- a/spec/search.spec.js +++ b/spec/search.spec.js @@ -1,7 +1,122 @@ const searchService = require('../src/services/search/search'); +const Note = require('../src/services/note_cache/entities/note'); +const Branch = require('../src/services/note_cache/entities/branch'); +const Attribute = require('../src/services/note_cache/entities/attribute'); +const ParsingContext = require('../src/services/search/parsing_context'); +const noteCache = require('../src/services/note_cache/note_cache'); +const randtoken = require('rand-token').generator({source: 'crypto'}); describe("Search", () => { - it("fulltext parser without content", () => { -// searchService. + let rootNote; + + beforeEach(() => { + noteCache.reset(); + + rootNote = new NoteBuilder(new Note(noteCache, {noteId: 'root', title: 'root'})); + new Branch(noteCache, {branchId: 'root', noteId: 'root', parentNoteId: 'none'}); + }); + + it("simple path match", async () => { + rootNote.child( + note("Europe") + .child( + note("Austria") + ) + ); + + const parsingContext = new ParsingContext(); + const searchResults = await searchService.findNotesWithQuery('europe austria', parsingContext); + + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + }); + + it("only end leafs are results", async () => { + rootNote.child( + note("Europe") + .child( + note("Austria") + ) + ); + + const parsingContext = new ParsingContext(); + const searchResults = await searchService.findNotesWithQuery('europe', parsingContext); + + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); + }); + + it("only end leafs are results", async () => { + rootNote.child( + note("Europe") + .child( + note("Austria") + .label('capital', 'Vienna') + ) + ); + + const parsingContext = new ParsingContext(); + + const searchResults = await searchService.findNotesWithQuery('Vienna', parsingContext); + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); }); }); + +/** @return {Note} */ +function findNoteByTitle(searchResults, title) { + return searchResults + .map(sr => noteCache.notes[sr.noteId]) + .find(note => note.title === title); +} + +class NoteBuilder { + constructor(note) { + this.note = note; + } + + label(name, value) { + new Attribute(noteCache, { + attributeId: id(), + noteId: this.note.noteId, + type: 'label', + name, + value + }); + + return this; + } + + relation(name, note) { + new Attribute(noteCache, { + attributeId: id(), + noteId: this.note.noteId, + type: 'relation', + name, + value: note.noteId + }); + + return this; + } + + child(childNoteBuilder, prefix = "") { + new Branch(noteCache, { + branchId: id(), + noteId: childNoteBuilder.note.noteId, + parentNoteId: this.note.noteId, + prefix + }); + + return this; + } +} + +function id() { + return randtoken.generate(10); +} + +function note(title) { + const note = new Note(noteCache, {noteId: id(), title}); + + return new NoteBuilder(note); +} diff --git a/src/services/note_cache/entities/attribute.js b/src/services/note_cache/entities/attribute.js index 173f80071..0de3c539f 100644 --- a/src/services/note_cache/entities/attribute.js +++ b/src/services/note_cache/entities/attribute.js @@ -17,6 +17,7 @@ class Attribute { /** @param {boolean} */ this.isInheritable = !!row.isInheritable; + this.noteCache.attributes[this.attributeId] = this; this.noteCache.notes[this.noteId].ownedAttributes.push(this); const key = `${this.type}-${this.name}`; diff --git a/src/services/note_cache/entities/branch.js b/src/services/note_cache/entities/branch.js index b3a3af904..dfff5a393 100644 --- a/src/services/note_cache/entities/branch.js +++ b/src/services/note_cache/entities/branch.js @@ -30,6 +30,7 @@ class Branch { parentNote.children.push(childNote); + this.noteCache.branches[this.branchId] = this; this.noteCache.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this; } diff --git a/src/services/note_cache/entities/note.js b/src/services/note_cache/entities/note.js index 8d3a7abc2..7556ce250 100644 --- a/src/services/note_cache/entities/note.js +++ b/src/services/note_cache/entities/note.js @@ -34,6 +34,8 @@ class Note { /** @param {string|null} */ this.flatTextCache = null; + this.noteCache.notes[this.noteId] = this; + if (protectedSessionService.isProtectedSessionAvailable()) { this.decrypt(); } diff --git a/src/services/note_cache/note_cache.js b/src/services/note_cache/note_cache.js index a39898750..c517563d6 100644 --- a/src/services/note_cache/note_cache.js +++ b/src/services/note_cache/note_cache.js @@ -6,16 +6,20 @@ const Attribute = require('./entities/attribute'); class NoteCache { constructor() { + this.reset(); + } + + reset() { /** @type {Object.} */ - this.notes = null; + this.notes = []; /** @type {Object.} */ - this.branches = null; + this.branches = []; /** @type {Object.} */ this.childParentToBranch = {}; /** @type {Object.} */ - this.attributes = null; + this.attributes = []; /** @type {Object.} Points from attribute type-name to list of attributes them */ - this.attributeIndex = null; + this.attributeIndex = {}; this.loaded = false; this.loadedResolve = null; diff --git a/src/services/note_cache/note_cache_loader.js b/src/services/note_cache/note_cache_loader.js index 9fb160128..b3f4893b9 100644 --- a/src/services/note_cache/note_cache_loader.js +++ b/src/services/note_cache/note_cache_loader.js @@ -11,34 +11,20 @@ const Attribute = require('./entities/attribute'); async function load() { await sqlInit.dbReady; - noteCache.notes = await getMappedRows(`SELECT noteId, title, isProtected FROM notes WHERE isDeleted = 0`, - row => new Note(noteCache, row)); + noteCache.reset(); - noteCache.branches = await getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`, - row => new Branch(noteCache, row)); + (await sql.getRows(`SELECT noteId, title, isProtected FROM notes WHERE isDeleted = 0`, [])) + .map(row => new Note(noteCache, row)); - noteCache.attributeIndex = []; + (await sql.getRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`, [])) + .map(row => new Branch(noteCache, row)); - noteCache.attributes = await getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`, - row => new Attribute(noteCache, row)); + (await sql.getRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`, [])).map(row => new Attribute(noteCache, row)); noteCache.loaded = true; noteCache.loadedResolve(); } -async function getMappedRows(query, cb) { - const map = {}; - const results = await sql.getRows(query, []); - - for (const row of results) { - const keys = Object.keys(row); - - map[row[keys[0]]] = cb(row); - } - - return map; -} - eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_SYNCED], async ({entityName, entity}) => { // note that entity can also be just POJO without methods if coming from sync diff --git a/src/services/search/expressions/and.js b/src/services/search/expressions/and.js index cbdd4e108..56973b11c 100644 --- a/src/services/search/expressions/and.js +++ b/src/services/search/expressions/and.js @@ -1,6 +1,8 @@ "use strict"; -class AndExp { +const Expression = require('./expression'); + +class AndExp extends Expression{ static of(subExpressions) { subExpressions = subExpressions.filter(exp => !!exp); @@ -12,6 +14,7 @@ class AndExp { } constructor(subExpressions) { + super(); this.subExpressions = subExpressions; } diff --git a/src/services/search/expressions/attribute_exists.js b/src/services/search/expressions/attribute_exists.js index 4f117a1fc..8b1da1b11 100644 --- a/src/services/search/expressions/attribute_exists.js +++ b/src/services/search/expressions/attribute_exists.js @@ -2,9 +2,12 @@ const NoteSet = require('../note_set'); const noteCache = require('../../note_cache/note_cache'); +const Expression = require('./expression'); -class AttributeExistsExp { +class AttributeExistsExp extends Expression { constructor(attributeType, attributeName, prefixMatch) { + super(); + this.attributeType = attributeType; this.attributeName = attributeName; this.prefixMatch = prefixMatch; diff --git a/src/services/search/expressions/expression.js b/src/services/search/expressions/expression.js new file mode 100644 index 000000000..49a1e64e5 --- /dev/null +++ b/src/services/search/expressions/expression.js @@ -0,0 +1,11 @@ +"use strict"; + +class Expression { + /** + * @param {NoteSet} noteSet + * @param {object} searchContext + */ + execute(noteSet, searchContext) {} +} + +module.exports = Expression; diff --git a/src/services/search/expressions/field_comparison.js b/src/services/search/expressions/field_comparison.js index cf3523d20..c497552d7 100644 --- a/src/services/search/expressions/field_comparison.js +++ b/src/services/search/expressions/field_comparison.js @@ -1,10 +1,13 @@ "use strict"; +const Expression = require('./expression'); const NoteSet = require('../note_set'); const noteCache = require('../../note_cache/note_cache'); -class FieldComparisonExp { +class FieldComparisonExp extends Expression { constructor(attributeType, attributeName, comparator) { + super(); + this.attributeType = attributeType; this.attributeName = attributeName; this.comparator = comparator; diff --git a/src/services/search/expressions/not.js b/src/services/search/expressions/not.js index 22d8ebee4..434a46dc6 100644 --- a/src/services/search/expressions/not.js +++ b/src/services/search/expressions/not.js @@ -1,7 +1,11 @@ "use strict"; -class NotExp { +const Expression = require('./expression'); + +class NotExp extends Expression { constructor(subExpression) { + super(); + this.subExpression = subExpression; } diff --git a/src/services/search/expressions/note_cache_fulltext.js b/src/services/search/expressions/note_cache_fulltext.js index 7a816f11b..93ec54328 100644 --- a/src/services/search/expressions/note_cache_fulltext.js +++ b/src/services/search/expressions/note_cache_fulltext.js @@ -1,10 +1,13 @@ "use strict"; +const Expression = require('./expression'); const NoteSet = require('../note_set'); const noteCache = require('../../note_cache/note_cache'); -class NoteCacheFulltextExp { +class NoteCacheFulltextExp extends Expression { constructor(tokens) { + super(); + this.tokens = tokens; } diff --git a/src/services/search/expressions/note_content_fulltext.js b/src/services/search/expressions/note_content_fulltext.js index 606dc780c..3148ded4d 100644 --- a/src/services/search/expressions/note_content_fulltext.js +++ b/src/services/search/expressions/note_content_fulltext.js @@ -1,10 +1,13 @@ "use strict"; +const Expression = require('./expression'); const NoteSet = require('../note_set'); const noteCache = require('../../note_cache/note_cache'); -class NoteContentFulltextExp { +class NoteContentFulltextExp extends Expression { constructor(tokens) { + super(); + this.tokens = tokens; } diff --git a/src/services/search/expressions/or.js b/src/services/search/expressions/or.js index 51406bcfc..3b21f24c5 100644 --- a/src/services/search/expressions/or.js +++ b/src/services/search/expressions/or.js @@ -1,8 +1,9 @@ "use strict"; +const Expression = require('./expression'); const NoteSet = require('../note_set'); -class OrExp { +class OrExp extends Expression { static of(subExpressions) { subExpressions = subExpressions.filter(exp => !!exp); @@ -15,6 +16,8 @@ class OrExp { } constructor(subExpressions) { + super(); + this.subExpressions = subExpressions; } diff --git a/src/services/search/lexer.js b/src/services/search/lexer.js index 301355d30..8a5b90f0a 100644 --- a/src/services/search/lexer.js +++ b/src/services/search/lexer.js @@ -1,4 +1,6 @@ function lexer(str) { + str = str.toLowerCase(); + const fulltextTokens = []; const expressionTokens = []; diff --git a/src/services/search/search.js b/src/services/search/search.js index 254c35869..5af43925b 100644 --- a/src/services/search/search.js +++ b/src/services/search/search.js @@ -11,6 +11,10 @@ const noteCacheService = require('../note_cache/note_cache_service'); const hoistedNoteService = require('../hoisted_note'); const utils = require('../utils'); +/** + * @param {Expression} expression + * @return {Promise} + */ async function findNotesWithExpression(expression) { const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()]; const allNotes = (hoistedNote && hoistedNote.noteId !== 'root') @@ -56,6 +60,21 @@ function parseQueryToExpression(query, parsingContext) { return expression; } +/** + * @param {string} query + * @param {ParsingContext} parsingContext + * @return {Promise} + */ +async function findNotesWithQuery(query, parsingContext) { + const expression = parseQueryToExpression(query, parsingContext); + + if (!expression) { + return []; + } + + return await findNotesWithExpression(expression); +} + async function searchNotesForAutocomplete(query) { if (!query.trim().length) { return []; @@ -66,13 +85,7 @@ async function searchNotesForAutocomplete(query) { fuzzyAttributeSearch: true }); - const expression = parseQueryToExpression(query, parsingContext); - - if (!expression) { - return []; - } - - let searchResults = await findNotesWithExpression(expression); + let searchResults = findNotesWithQuery(query, parsingContext); searchResults = searchResults.slice(0, 200); @@ -141,5 +154,6 @@ function formatAttribute(attr) { } module.exports = { - searchNotesForAutocomplete + searchNotesForAutocomplete, + findNotesWithQuery };