trilium/spec/search.spec.js
2020-05-27 00:09:19 +02:00

533 lines
22 KiB
JavaScript

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 dateUtils = require('../src/services/date_utils');
const noteCache = require('../src/services/note_cache/note_cache');
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking');
describe("Search", () => {
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();
});
it("label comparison with short syntax", async () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.label('capital', 'Vienna'))
.child(note("Czech Republic")
.label('capital', 'Prague'))
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('#capital=Vienna', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("label comparison with full syntax", async () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.label('capital', 'Vienna'))
.child(note("Czech Republic")
.label('capital', 'Prague'))
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# note.labels.capital=Prague', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("numeric label comparison", async () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('population', '8859000'))
.child(note("Czech Republic")
.label('population', '10650000'))
);
const parsingContext = new ParsingContext();
const searchResults = await searchService.findNotesWithQuery('#country #population >= 10000000', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("numeric label comparison fallback to string comparison", async () => {
// dates should not be coerced into numbers which would then give wrong numbers
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('established', '1955-07-27'))
.child(note("Czech Republic")
.label('established', '1993-01-01'))
.child(note("Hungary")
.label('established', '1920-06-04'))
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('#established <= 1955-01-01', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Hungary")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('#established > 1955-01-01', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("smart date comparisons", async () => {
// dates should not be coerced into numbers which would then give wrong numbers
rootNote
.child(note("My note")
.label('year', new Date().getFullYear().toString())
.label('month', dateUtils.localNowDate().substr(0, 7))
.label('date', dateUtils.localNowDate())
.label('dateTime', dateUtils.localNowDateTime())
);
const parsingContext = new ParsingContext();
async function test(query, expectedResultCount) {
const searchResults = await searchService.findNotesWithQuery(query, parsingContext);
expect(searchResults.length).toEqual(expectedResultCount);
if (expectedResultCount === 1) {
expect(findNoteByTitle(searchResults, "My note")).toBeTruthy();
}
}
await test("#year = YEAR", 1);
await test("#year >= YEAR", 1);
await test("#year <= YEAR", 1);
await test("#year < YEAR+1", 1);
await test("#year > YEAR+1", 0);
await test("#month = MONTH", 1);
await test("#date = TODAY", 1);
await test("#date > TODAY", 0);
await test("#date > TODAY-1", 1);
await test("#date < TODAY+1", 1);
await test("#date < 'TODAY + 1'", 1);
await test("#dateTime <= NOW+10", 1);
await test("#dateTime < NOW-10", 0);
await test("#dateTime >= NOW-10", 1);
await test("#dateTime < NOW-10", 0);
});
it("logical or", async () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('languageFamily', 'germanic'))
.child(note("Czech Republic")
.label('languageFamily', 'slavic'))
.child(note("Hungary")
.label('languageFamily', 'finnougric'))
);
const parsingContext = new ParsingContext();
const searchResults = await searchService.findNotesWithQuery('#languageFamily = slavic OR #languageFamily = germanic', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("fuzzy attribute search", async () => {
rootNote
.child(note("Europe")
.label('country', '', true)
.child(note("Austria")
.label('languageFamily', 'germanic'))
.child(note("Czech Republic")
.label('languageFamily', 'slavic')));
let parsingContext = new ParsingContext({fuzzyAttributeSearch: false});
let searchResults = await searchService.findNotesWithQuery('#language', parsingContext);
expect(searchResults.length).toEqual(0);
searchResults = await searchService.findNotesWithQuery('#languageFamily=ger', parsingContext);
expect(searchResults.length).toEqual(0);
parsingContext = new ParsingContext({fuzzyAttributeSearch: true});
searchResults = await searchService.findNotesWithQuery('#language', parsingContext);
expect(searchResults.length).toEqual(2);
searchResults = await searchService.findNotesWithQuery('#languageFamily=ger', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("filter by note property", async () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
.child(note("Czech Republic")));
const parsingContext = new ParsingContext();
const searchResults = await searchService.findNotesWithQuery('# note.title =* czech', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("filter by note's parent", async () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
.child(note("Czech Republic")
.child(note("Prague")))
)
.child(note("Asia")
.child(note('Taiwan')));
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# note.parents.title = Asia', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Taiwan")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# note.parents.parents.title = Europe', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
});
it("filter by note's ancestor", async () => {
rootNote
.child(note("Europe")
.child(note("Austria"))
.child(note("Czech Republic")
.child(note("Prague").label('city')))
)
.child(note("Asia")
.child(note('Taiwan')
.child(note('Taipei').label('city')))
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('#city AND note.ancestors.title = Europe', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('#city AND note.ancestors.title = Asia', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy();
});
it("filter by note's child", async () => {
rootNote
.child(note("Europe")
.child(note("Austria")
.child(note("Vienna")))
.child(note("Czech Republic")
.child(note("Prague"))))
.child(note("Oceania")
.child(note('Australia')));
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# note.children.title =* Aust', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Oceania")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# note.children.title =* Aust AND note.children.title *= republic', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# note.children.children.title = Prague', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
});
it("filter by relation's note properties using short syntax", 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();
});
it("filter by relation's note properties using long syntax", 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();
const searchResults = await searchService.findNotesWithQuery('# note.relations.neighbor.title = Austria', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("filter by multiple level relation", async () => {
const austria = note("Austria");
const slovakia = note("Slovakia");
const italy = note("Italy");
const ukraine = note("Ukraine");
rootNote
.child(note("Europe")
.child(austria
.relation('neighbor', italy.note)
.relation('neighbor', slovakia.note))
.child(note("Czech Republic")
.relation('neighbor', austria.note)
.relation('neighbor', slovakia.note))
.child(slovakia
.relation('neighbor', ukraine.note))
.child(ukraine)
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# note.relations.neighbor.relations.neighbor.title = Italy', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# note.relations.neighbor.relations.neighbor.title = Ukraine', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("test note properties", async () => {
const austria = note("Austria");
austria.relation('myself', austria.note);
austria.label('capital', 'Vienna');
austria.label('population', '8859000');
rootNote
.child(note("Asia"))
.child(note("Europe")
.child(austria
.child(note("Vienna"))
.child(note("Sebastian Kurz"))
)
)
.child(note("Mozart")
.child(austria));
austria.note.type = 'text';
austria.note.mime = 'text/html';
austria.note.isProtected = false;
austria.note.dateCreated = '2020-05-14 12:11:42.001+0200';
austria.note.dateModified = '2020-05-14 13:11:42.001+0200';
austria.note.utcDateCreated = '2020-05-14 10:11:42.001Z';
austria.note.utcDateModified = '2020-05-14 11:11:42.001Z';
austria.note.contentLength = 1001;
const parsingContext = new ParsingContext();
async function test(propertyName, value, expectedResultCount) {
const searchResults = await searchService.findNotesWithQuery(`# note.${propertyName} = ${value}`, parsingContext);
expect(searchResults.length).toEqual(expectedResultCount);
if (expectedResultCount === 1) {
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}
}
await test("type", "text", 1);
await test("type", "code", 0);
await test("mime", "text/html", 1);
await test("mime", "application/json", 0);
await test("isProtected", "false", 7);
await test("isProtected", "true", 0);
await test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1);
await test("dateCreated", "wrong", 0);
await test("dateModified", "'2020-05-14 13:11:42.001+0200'", 1);
await test("dateModified", "wrong", 0);
await test("utcDateCreated", "'2020-05-14 10:11:42.001Z'", 1);
await test("utcDateCreated", "wrong", 0);
await test("utcDateModified", "'2020-05-14 11:11:42.001Z'", 1);
await test("utcDateModified", "wrong", 0);
await test("contentLength", "1001", 1);
await test("contentLength", "10010", 0);
await test("parentCount", "2", 1);
await test("parentCount", "3", 0);
await test("childrenCount", "2", 1);
await test("childrenCount", "10", 0);
await test("attributeCount", "3", 1);
await test("attributeCount", "4", 0);
await test("labelCount", "2", 1);
await test("labelCount", "3", 0);
await test("relationCount", "1", 1);
await test("relationCount", "2", 0);
});
it("test order by", async () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
const austria = note("Austria").label("capital", "Vienna");
const ukraine = note("Ukraine").label("capital", "Kiev");
rootNote
.child(note("Europe")
.child(ukraine)
.child(slovakia)
.child(austria)
.child(italy));
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.title', parsingContext);
expect(searchResults.length).toEqual(4);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
expect(noteCache.notes[searchResults[2].noteId].title).toEqual("Slovakia");
expect(noteCache.notes[searchResults[3].noteId].title).toEqual("Ukraine");
searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.labels.capital', parsingContext);
expect(searchResults.length).toEqual(4);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Slovakia");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Ukraine");
expect(noteCache.notes[searchResults[2].noteId].title).toEqual("Italy");
expect(noteCache.notes[searchResults[3].noteId].title).toEqual("Austria");
searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC', parsingContext);
expect(searchResults.length).toEqual(4);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
expect(noteCache.notes[searchResults[2].noteId].title).toEqual("Ukraine");
expect(noteCache.notes[searchResults[3].noteId].title).toEqual("Slovakia");
searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC limit 2', parsingContext);
expect(searchResults.length).toEqual(2);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(noteCache.notes[searchResults[1].noteId].title).toEqual("Italy");
searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 0', parsingContext);
expect(searchResults.length).toEqual(0);
searchResults = await searchService.findNotesWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', parsingContext);
expect(searchResults.length).toEqual(4);
});
it("test not(...)", async () => {
const italy = note("Italy").label("capital", "Rome");
const slovakia = note("Slovakia").label("capital", "Bratislava");
rootNote
.child(note("Europe")
.child(slovakia)
.child(italy));
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# not(#capital) and note.noteId != root', parsingContext);
expect(searchResults.length).toEqual(1);
expect(noteCache.notes[searchResults[0].noteId].title).toEqual("Europe");
});
// FIXME: test what happens when we order without any filter criteria
});