mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 13:01:31 +08:00 
			
		
		
		
	added querying by relation's properties
This commit is contained in:
		
							parent
							
								
									3d12341ff1
								
							
						
					
					
						commit
						355ffd3d02
					
				| @ -191,6 +191,31 @@ describe("Search", () => { | |||||||
|         expect(searchResults.length).toEqual(1); |         expect(searchResults.length).toEqual(1); | ||||||
|         expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); |         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} */ | /** @return {Note} */ | ||||||
| @ -218,13 +243,13 @@ class NoteBuilder { | |||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     relation(name, note) { |     relation(name, targetNote) { | ||||||
|         new Attribute(noteCache, { |         new Attribute(noteCache, { | ||||||
|             attributeId: id(), |             attributeId: id(), | ||||||
|             noteId: this.note.noteId, |             noteId: this.note.noteId, | ||||||
|             type: 'relation', |             type: 'relation', | ||||||
|             name, |             name, | ||||||
|             value: note.noteId |             value: targetNote.noteId | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         return this; |         return this; | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ class Attribute { | |||||||
|         /** @param {string} */ |         /** @param {string} */ | ||||||
|         this.name = row.name.toLowerCase(); |         this.name = row.name.toLowerCase(); | ||||||
|         /** @param {string} */ |         /** @param {string} */ | ||||||
|         this.value = row.value.toLowerCase(); |         this.value = row.type === 'label'? row.value.toLowerCase() : row.value; | ||||||
|         /** @param {boolean} */ |         /** @param {boolean} */ | ||||||
|         this.isInheritable = !!row.isInheritable; |         this.isInheritable = !!row.isInheritable; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -18,12 +18,12 @@ class AndExp extends Expression { | |||||||
|         this.subExpressions = subExpressions; |         this.subExpressions = subExpressions; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet, searchContext) { |     execute(inputNoteSet, searchContext) { | ||||||
|         for (const subExpression of this.subExpressions) { |         for (const subExpression of this.subExpressions) { | ||||||
|             noteSet = subExpression.execute(noteSet, searchContext); |             inputNoteSet = subExpression.execute(inputNoteSet, searchContext); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return noteSet; |         return inputNoteSet; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ class AttributeExistsExp extends Expression { | |||||||
|         this.prefixMatch = prefixMatch; |         this.prefixMatch = prefixMatch; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet) { |     execute(inputNoteSet) { | ||||||
|         const attrs = this.prefixMatch |         const attrs = this.prefixMatch | ||||||
|             ? noteCache.findAttributesWithPrefix(this.attributeType, this.attributeName) |             ? noteCache.findAttributesWithPrefix(this.attributeType, this.attributeName) | ||||||
|             : noteCache.findAttributes(this.attributeType, this.attributeName); |             : noteCache.findAttributes(this.attributeType, this.attributeName); | ||||||
| @ -23,7 +23,7 @@ class AttributeExistsExp extends Expression { | |||||||
|         for (const attr of attrs) { |         for (const attr of attrs) { | ||||||
|             const note = attr.note; |             const note = attr.note; | ||||||
| 
 | 
 | ||||||
|             if (noteSet.hasNoteId(note.noteId)) { |             if (inputNoteSet.hasNoteId(note.noteId)) { | ||||||
|                 if (attr.isInheritable) { |                 if (attr.isInheritable) { | ||||||
|                     resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); |                     resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -2,11 +2,11 @@ | |||||||
| 
 | 
 | ||||||
| class Expression { | class Expression { | ||||||
|     /** |     /** | ||||||
|      * @param {NoteSet} noteSet |      * @param {NoteSet} inputNoteSet | ||||||
|      * @param {object} searchContext |      * @param {object} searchContext | ||||||
|      * @return {NoteSet} |      * @return {NoteSet} | ||||||
|      */ |      */ | ||||||
|     execute(noteSet, searchContext) {} |     execute(inputNoteSet, searchContext) {} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| module.exports = Expression; | module.exports = Expression; | ||||||
|  | |||||||
| @ -13,14 +13,14 @@ class LabelComparisonExp extends Expression { | |||||||
|         this.comparator = comparator; |         this.comparator = comparator; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet) { |     execute(inputNoteSet) { | ||||||
|         const attrs = noteCache.findAttributes(this.attributeType, this.attributeName); |         const attrs = noteCache.findAttributes(this.attributeType, this.attributeName); | ||||||
|         const resultNoteSet = new NoteSet(); |         const resultNoteSet = new NoteSet(); | ||||||
| 
 | 
 | ||||||
|         for (const attr of attrs) { |         for (const attr of attrs) { | ||||||
|             const note = attr.note; |             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) { |                 if (attr.isInheritable) { | ||||||
|                     resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); |                     resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); | ||||||
|                 } |                 } | ||||||
|  | |||||||
| @ -9,10 +9,10 @@ class NotExp extends Expression { | |||||||
|         this.subExpression = subExpression; |         this.subExpression = subExpression; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet, searchContext) { |     execute(inputNoteSet, searchContext) { | ||||||
|         const subNoteSet = this.subExpression.execute(noteSet, searchContext); |         const subNoteSet = this.subExpression.execute(inputNoteSet, searchContext); | ||||||
| 
 | 
 | ||||||
|         return noteSet.minus(subNoteSet); |         return inputNoteSet.minus(subNoteSet); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ class NoteCacheFulltextExp extends Expression { | |||||||
|         this.tokens = tokens; |         this.tokens = tokens; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet, searchContext) { |     execute(inputNoteSet, searchContext) { | ||||||
|         // has deps on SQL which breaks unit test so needs to be dynamically required
 |         // has deps on SQL which breaks unit test so needs to be dynamically required
 | ||||||
|         const noteCacheService = require('../../note_cache/note_cache_service'); |         const noteCacheService = require('../../note_cache/note_cache_service'); | ||||||
|         const resultNoteSet = new NoteSet(); |         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) { |         for (const note of candidateNotes) { | ||||||
|             // autocomplete should be able to find notes by their noteIds as well (only leafs)
 |             // autocomplete should be able to find notes by their noteIds as well (only leafs)
 | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ class NoteContentFulltextExp extends Expression { | |||||||
|         this.tokens = tokens; |         this.tokens = tokens; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async execute(noteSet) { |     async execute(inputNoteSet) { | ||||||
|         const resultNoteSet = new NoteSet(); |         const resultNoteSet = new NoteSet(); | ||||||
|         const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike('%', token, '%')); |         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 ')}`);
 |             WHERE isDeleted = 0 AND isProtected = 0 AND ${wheres.join(' AND ')}`);
 | ||||||
| 
 | 
 | ||||||
|         for (const noteId of noteIds) { |         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]); |                 resultNoteSet.add(noteCache.notes[noteId]); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -21,11 +21,11 @@ class OrExp extends Expression { | |||||||
|         this.subExpressions = subExpressions; |         this.subExpressions = subExpressions; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet, searchContext) { |     execute(inputNoteSet, searchContext) { | ||||||
|         const resultNoteSet = new NoteSet(); |         const resultNoteSet = new NoteSet(); | ||||||
| 
 | 
 | ||||||
|         for (const subExpression of this.subExpressions) { |         for (const subExpression of this.subExpressions) { | ||||||
|             resultNoteSet.mergeIn(subExpression.execute(noteSet, searchContext)); |             resultNoteSet.mergeIn(subExpression.execute(inputNoteSet, searchContext)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return resultNoteSet; |         return resultNoteSet; | ||||||
|  | |||||||
| @ -11,10 +11,10 @@ class PropertyComparisonExp extends Expression { | |||||||
|         this.comparator = comparator; |         this.comparator = comparator; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     execute(noteSet, searchContext) { |     execute(inputNoteSet, searchContext) { | ||||||
|         const resNoteSet = new NoteSet(); |         const resNoteSet = new NoteSet(); | ||||||
| 
 | 
 | ||||||
|         for (const note of noteSet.notes) { |         for (const note of inputNoteSet.notes) { | ||||||
|             const value = note[this.propertyName].toLowerCase(); |             const value = note[this.propertyName].toLowerCase(); | ||||||
| 
 | 
 | ||||||
|             if (this.comparator(value)) { |             if (this.comparator(value)) { | ||||||
|  | |||||||
							
								
								
									
										41
									
								
								src/services/search/expressions/relation_where.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/services/search/expressions/relation_where.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
| @ -41,6 +41,18 @@ class NoteSet { | |||||||
| 
 | 
 | ||||||
|         return newNoteSet; |         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; | module.exports = NoteSet; | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ const OrExp = require('./expressions/or'); | |||||||
| const NotExp = require('./expressions/not'); | const NotExp = require('./expressions/not'); | ||||||
| const ChildOfExp = require('./expressions/child_of'); | const ChildOfExp = require('./expressions/child_of'); | ||||||
| const ParentOfExp = require('./expressions/parent_of'); | const ParentOfExp = require('./expressions/parent_of'); | ||||||
|  | const RelationWhereExp = require('./expressions/relation_where'); | ||||||
| const PropertyComparisonExp = require('./expressions/property_comparison'); | const PropertyComparisonExp = require('./expressions/property_comparison'); | ||||||
| const AttributeExistsExp = require('./expressions/attribute_exists'); | const AttributeExistsExp = require('./expressions/attribute_exists'); | ||||||
| const LabelComparisonExp = require('./expressions/label_comparison'); | const LabelComparisonExp = require('./expressions/label_comparison'); | ||||||
| @ -90,10 +91,9 @@ function getExpression(tokens, parsingContext) { | |||||||
|         if (Array.isArray(token)) { |         if (Array.isArray(token)) { | ||||||
|             expressions.push(getExpression(token, parsingContext)); |             expressions.push(getExpression(token, parsingContext)); | ||||||
|         } |         } | ||||||
|         else if (token.startsWith('#') || token.startsWith('~')) { |         else if (token.startsWith('#')) { | ||||||
|             const type = token.startsWith('#') ? 'label' : 'relation'; |             const labelName = token.substr(1); | ||||||
| 
 |             parsingContext.highlightedTokens.push(labelName); | ||||||
|             parsingContext.highlightedTokens.push(token.substr(1)); |  | ||||||
| 
 | 
 | ||||||
|             if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { |             if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { | ||||||
|                 let operator = tokens[i + 1]; |                 let operator = tokens[i + 1]; | ||||||
| @ -112,12 +112,25 @@ function getExpression(tokens, parsingContext) { | |||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 expressions.push(new LabelComparisonExp(type, token.substr(1), comparator)); |                 expressions.push(new LabelComparisonExp('label', labelName, comparator)); | ||||||
| 
 | 
 | ||||||
|                 i += 2; |                 i += 2; | ||||||
|             } |             } | ||||||
|             else { |             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') { |         else if (token === 'note') { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam