From 355ffd3d029e6a2fc609c8e8cfe3e35afcdf1c5c Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 23 May 2020 12:27:44 +0200 Subject: [PATCH] added querying by relation's properties --- spec/search.spec.js | 29 ++++++++++++- src/services/note_cache/entities/attribute.js | 2 +- src/services/search/expressions/and.js | 6 +-- .../search/expressions/attribute_exists.js | 4 +- src/services/search/expressions/expression.js | 4 +- .../search/expressions/label_comparison.js | 4 +- src/services/search/expressions/not.js | 6 +-- .../search/expressions/note_cache_fulltext.js | 4 +- .../expressions/note_content_fulltext.js | 4 +- src/services/search/expressions/or.js | 4 +- .../search/expressions/property_comparison.js | 4 +- .../search/expressions/relation_where.js | 41 +++++++++++++++++++ src/services/search/note_set.js | 12 ++++++ src/services/search/parser.js | 25 ++++++++--- 14 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 src/services/search/expressions/relation_where.js diff --git a/spec/search.spec.js b/spec/search.spec.js index 053c17c44..8a4589499 100644 --- a/spec/search.spec.js +++ b/spec/search.spec.js @@ -191,6 +191,31 @@ describe("Search", () => { expect(searchResults.length).toEqual(1); expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); }); + + it("filter by relation's note properties", async () => { + const austria = note("Austria"); + const portugal = note("Portugal"); + + rootNote + .child(note("Europe") + .child(austria) + .child(note("Czech Republic") + .relation('neighbor', austria.note)) + .child(portugal) + .child(note("Spain") + .relation('neighbor', portugal.note)) + ); + + const parsingContext = new ParsingContext(); + + let searchResults = await searchService.findNotesWithQuery('# ~neighbor.title = Austria', parsingContext); + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + + searchResults = await searchService.findNotesWithQuery('# ~neighbor.title = Portugal', parsingContext); + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Spain")).toBeTruthy(); + }); }); /** @return {Note} */ @@ -218,13 +243,13 @@ class NoteBuilder { return this; } - relation(name, note) { + relation(name, targetNote) { new Attribute(noteCache, { attributeId: id(), noteId: this.note.noteId, type: 'relation', name, - value: note.noteId + value: targetNote.noteId }); return this; diff --git a/src/services/note_cache/entities/attribute.js b/src/services/note_cache/entities/attribute.js index 0de3c539f..73ae80e86 100644 --- a/src/services/note_cache/entities/attribute.js +++ b/src/services/note_cache/entities/attribute.js @@ -13,7 +13,7 @@ class Attribute { /** @param {string} */ this.name = row.name.toLowerCase(); /** @param {string} */ - this.value = row.value.toLowerCase(); + this.value = row.type === 'label'? row.value.toLowerCase() : row.value; /** @param {boolean} */ this.isInheritable = !!row.isInheritable; diff --git a/src/services/search/expressions/and.js b/src/services/search/expressions/and.js index b5991503f..ee22f6b13 100644 --- a/src/services/search/expressions/and.js +++ b/src/services/search/expressions/and.js @@ -18,12 +18,12 @@ class AndExp extends Expression { this.subExpressions = subExpressions; } - execute(noteSet, searchContext) { + execute(inputNoteSet, searchContext) { for (const subExpression of this.subExpressions) { - noteSet = subExpression.execute(noteSet, searchContext); + inputNoteSet = subExpression.execute(inputNoteSet, searchContext); } - return noteSet; + return inputNoteSet; } } diff --git a/src/services/search/expressions/attribute_exists.js b/src/services/search/expressions/attribute_exists.js index 8b1da1b11..b368ab92a 100644 --- a/src/services/search/expressions/attribute_exists.js +++ b/src/services/search/expressions/attribute_exists.js @@ -13,7 +13,7 @@ class AttributeExistsExp extends Expression { this.prefixMatch = prefixMatch; } - execute(noteSet) { + execute(inputNoteSet) { const attrs = this.prefixMatch ? noteCache.findAttributesWithPrefix(this.attributeType, this.attributeName) : noteCache.findAttributes(this.attributeType, this.attributeName); @@ -23,7 +23,7 @@ class AttributeExistsExp extends Expression { for (const attr of attrs) { const note = attr.note; - if (noteSet.hasNoteId(note.noteId)) { + if (inputNoteSet.hasNoteId(note.noteId)) { if (attr.isInheritable) { resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); } diff --git a/src/services/search/expressions/expression.js b/src/services/search/expressions/expression.js index 48c247152..41192cbb4 100644 --- a/src/services/search/expressions/expression.js +++ b/src/services/search/expressions/expression.js @@ -2,11 +2,11 @@ class Expression { /** - * @param {NoteSet} noteSet + * @param {NoteSet} inputNoteSet * @param {object} searchContext * @return {NoteSet} */ - execute(noteSet, searchContext) {} + execute(inputNoteSet, searchContext) {} } module.exports = Expression; diff --git a/src/services/search/expressions/label_comparison.js b/src/services/search/expressions/label_comparison.js index b69d07102..143c41b6e 100644 --- a/src/services/search/expressions/label_comparison.js +++ b/src/services/search/expressions/label_comparison.js @@ -13,14 +13,14 @@ class LabelComparisonExp extends Expression { this.comparator = comparator; } - execute(noteSet) { + execute(inputNoteSet) { const attrs = noteCache.findAttributes(this.attributeType, this.attributeName); const resultNoteSet = new NoteSet(); for (const attr of attrs) { const note = attr.note; - if (noteSet.hasNoteId(note.noteId) && this.comparator(attr.value)) { + if (inputNoteSet.hasNoteId(note.noteId) && this.comparator(attr.value)) { if (attr.isInheritable) { resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); } diff --git a/src/services/search/expressions/not.js b/src/services/search/expressions/not.js index 434a46dc6..a24d3c2c1 100644 --- a/src/services/search/expressions/not.js +++ b/src/services/search/expressions/not.js @@ -9,10 +9,10 @@ class NotExp extends Expression { this.subExpression = subExpression; } - execute(noteSet, searchContext) { - const subNoteSet = this.subExpression.execute(noteSet, searchContext); + execute(inputNoteSet, searchContext) { + const subNoteSet = this.subExpression.execute(inputNoteSet, searchContext); - return noteSet.minus(subNoteSet); + return inputNoteSet.minus(subNoteSet); } } diff --git a/src/services/search/expressions/note_cache_fulltext.js b/src/services/search/expressions/note_cache_fulltext.js index 93ec54328..eedc3e279 100644 --- a/src/services/search/expressions/note_cache_fulltext.js +++ b/src/services/search/expressions/note_cache_fulltext.js @@ -11,7 +11,7 @@ class NoteCacheFulltextExp extends Expression { this.tokens = tokens; } - execute(noteSet, searchContext) { + execute(inputNoteSet, 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(); @@ -66,7 +66,7 @@ class NoteCacheFulltextExp extends Expression { } } - const candidateNotes = this.getCandidateNotes(noteSet); + const candidateNotes = this.getCandidateNotes(inputNoteSet); for (const note of candidateNotes) { // autocomplete should be able to find notes by their noteIds as well (only leafs) diff --git a/src/services/search/expressions/note_content_fulltext.js b/src/services/search/expressions/note_content_fulltext.js index 3148ded4d..94932cde7 100644 --- a/src/services/search/expressions/note_content_fulltext.js +++ b/src/services/search/expressions/note_content_fulltext.js @@ -11,7 +11,7 @@ class NoteContentFulltextExp extends Expression { this.tokens = tokens; } - async execute(noteSet) { + async execute(inputNoteSet) { const resultNoteSet = new NoteSet(); const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike('%', token, '%')); @@ -24,7 +24,7 @@ class NoteContentFulltextExp extends Expression { WHERE isDeleted = 0 AND isProtected = 0 AND ${wheres.join(' AND ')}`); for (const noteId of noteIds) { - if (noteSet.hasNoteId(noteId) && noteId in noteCache.notes) { + if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) { resultNoteSet.add(noteCache.notes[noteId]); } } diff --git a/src/services/search/expressions/or.js b/src/services/search/expressions/or.js index 3b21f24c5..62c16f5cf 100644 --- a/src/services/search/expressions/or.js +++ b/src/services/search/expressions/or.js @@ -21,11 +21,11 @@ class OrExp extends Expression { this.subExpressions = subExpressions; } - execute(noteSet, searchContext) { + execute(inputNoteSet, searchContext) { const resultNoteSet = new NoteSet(); for (const subExpression of this.subExpressions) { - resultNoteSet.mergeIn(subExpression.execute(noteSet, searchContext)); + resultNoteSet.mergeIn(subExpression.execute(inputNoteSet, searchContext)); } return resultNoteSet; diff --git a/src/services/search/expressions/property_comparison.js b/src/services/search/expressions/property_comparison.js index 1ca544aa0..730bf5597 100644 --- a/src/services/search/expressions/property_comparison.js +++ b/src/services/search/expressions/property_comparison.js @@ -11,10 +11,10 @@ class PropertyComparisonExp extends Expression { this.comparator = comparator; } - execute(noteSet, searchContext) { + execute(inputNoteSet, searchContext) { const resNoteSet = new NoteSet(); - for (const note of noteSet.notes) { + for (const note of inputNoteSet.notes) { const value = note[this.propertyName].toLowerCase(); if (this.comparator(value)) { diff --git a/src/services/search/expressions/relation_where.js b/src/services/search/expressions/relation_where.js new file mode 100644 index 000000000..f873762e0 --- /dev/null +++ b/src/services/search/expressions/relation_where.js @@ -0,0 +1,41 @@ +"use strict"; + +const Expression = require('./expression'); +const NoteSet = require('../note_set'); +const noteCache = require('../../note_cache/note_cache'); + +class RelationWhereExp extends Expression { + constructor(relationName, subExpression) { + super(); + + this.relationName = relationName; + this.subExpression = subExpression; + } + + execute(inputNoteSet, searchContext) { + const candidateNoteSet = new NoteSet(); + + for (const attr of noteCache.findAttributes('relation', this.relationName)) { + const note = attr.note; + + if (inputNoteSet.hasNoteId(note.noteId)) { + const subInputNoteSet = new NoteSet([attr.targetNote]); + const subResNoteSet = this.subExpression.execute(subInputNoteSet, searchContext); + + if (subResNoteSet.hasNote(attr.targetNote)) { + if (attr.isInheritable) { + candidateNoteSet.addAll(note.subtreeNotesIncludingTemplated); + } else if (note.isTemplate) { + candidateNoteSet.addAll(note.templatedNotes); + } else { + candidateNoteSet.add(note); + } + } + } + } + + return candidateNoteSet.intersection(inputNoteSet); + } +} + +module.exports = RelationWhereExp; diff --git a/src/services/search/note_set.js b/src/services/search/note_set.js index 40b38de35..85c402a29 100644 --- a/src/services/search/note_set.js +++ b/src/services/search/note_set.js @@ -41,6 +41,18 @@ class NoteSet { return newNoteSet; } + + intersection(anotherNoteSet) { + const newNoteSet = new NoteSet(); + + for (const note of this.notes) { + if (anotherNoteSet.hasNote(note)) { + newNoteSet.add(note); + } + } + + return newNoteSet; + } } module.exports = NoteSet; diff --git a/src/services/search/parser.js b/src/services/search/parser.js index 9ca4b149c..bb03b2d73 100644 --- a/src/services/search/parser.js +++ b/src/services/search/parser.js @@ -5,6 +5,7 @@ const OrExp = require('./expressions/or'); const NotExp = require('./expressions/not'); const ChildOfExp = require('./expressions/child_of'); const ParentOfExp = require('./expressions/parent_of'); +const RelationWhereExp = require('./expressions/relation_where'); const PropertyComparisonExp = require('./expressions/property_comparison'); const AttributeExistsExp = require('./expressions/attribute_exists'); const LabelComparisonExp = require('./expressions/label_comparison'); @@ -90,10 +91,9 @@ function getExpression(tokens, parsingContext) { if (Array.isArray(token)) { expressions.push(getExpression(token, parsingContext)); } - else if (token.startsWith('#') || token.startsWith('~')) { - const type = token.startsWith('#') ? 'label' : 'relation'; - - parsingContext.highlightedTokens.push(token.substr(1)); + else if (token.startsWith('#')) { + const labelName = token.substr(1); + parsingContext.highlightedTokens.push(labelName); if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { let operator = tokens[i + 1]; @@ -112,12 +112,25 @@ function getExpression(tokens, parsingContext) { continue; } - expressions.push(new LabelComparisonExp(type, token.substr(1), comparator)); + expressions.push(new LabelComparisonExp('label', labelName, comparator)); i += 2; } else { - expressions.push(new AttributeExistsExp(type, token.substr(1), parsingContext.fuzzyAttributeSearch)); + expressions.push(new AttributeExistsExp('label', labelName, parsingContext.fuzzyAttributeSearch)); + } + } + else if (token.startsWith('~')) { + const relationName = token.substr(1); + parsingContext.highlightedTokens.push(relationName); + + if (i < tokens.length - 2 && tokens[i + 1] === '.') { + i += 1; + + expressions.push(new RelationWhereExp(relationName, parseNoteProperty())); + } + else { + expressions.push(new AttributeExistsExp('relation', relationName, parsingContext.fuzzyAttributeSearch)); } } else if (token === 'note') {