mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02: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