mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
search now supports searching / ordering by note size
This commit is contained in:
parent
480aec1667
commit
872e81fe1f
@ -172,9 +172,6 @@ class TreeCache {
|
|||||||
throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`);
|
throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// force to load all the notes at once instead of one by one
|
|
||||||
await this.getNotes(searchResultNoteIds);
|
|
||||||
|
|
||||||
// reset all the virtual branches from old search results
|
// reset all the virtual branches from old search results
|
||||||
if (note.noteId in treeCache.notes) {
|
if (note.noteId in treeCache.notes) {
|
||||||
treeCache.notes[note.noteId].children = [];
|
treeCache.notes[note.noteId].children = [];
|
||||||
|
@ -164,6 +164,9 @@ const TPL = `
|
|||||||
<option value="title">Title</option>
|
<option value="title">Title</option>
|
||||||
<option value="dateCreated">Date created</option>
|
<option value="dateCreated">Date created</option>
|
||||||
<option value="dateModified">Date of last modification</option>
|
<option value="dateModified">Date of last modification</option>
|
||||||
|
<option value="contentSize">Note content size</option>
|
||||||
|
<option value="noteSize">Note content size including revisions</option>
|
||||||
|
<option value="revisionCount">Number of revisions</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select name="orderDirection" class="form-control w-auto d-inline">
|
<select name="orderDirection" class="form-control w-auto d-inline">
|
||||||
|
@ -15,7 +15,7 @@ async function search(note) {
|
|||||||
|
|
||||||
if (searchScript) {
|
if (searchScript) {
|
||||||
searchResultNoteIds = await searchFromRelation(note, 'searchScript');
|
searchResultNoteIds = await searchFromRelation(note, 'searchScript');
|
||||||
} else if (searchString) {
|
} else {
|
||||||
const searchContext = new SearchContext({
|
const searchContext = new SearchContext({
|
||||||
fastSearch: note.hasLabel('fastSearch'),
|
fastSearch: note.hasLabel('fastSearch'),
|
||||||
ancestorNoteId: note.getRelationValue('ancestor'),
|
ancestorNoteId: note.getRelationValue('ancestor'),
|
||||||
@ -27,8 +27,6 @@ async function search(note) {
|
|||||||
|
|
||||||
searchResultNoteIds = searchService.findNotesWithQuery(searchString, searchContext)
|
searchResultNoteIds = searchService.findNotesWithQuery(searchString, searchContext)
|
||||||
.map(sr => sr.noteId);
|
.map(sr => sr.noteId);
|
||||||
} else {
|
|
||||||
searchResultNoteIds = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we won't return search note's own noteId
|
// we won't return search note's own noteId
|
||||||
|
@ -30,6 +30,15 @@ class Note {
|
|||||||
|
|
||||||
/** @param {Note[]|null} */
|
/** @param {Note[]|null} */
|
||||||
this.ancestorCache = null;
|
this.ancestorCache = null;
|
||||||
|
|
||||||
|
// following attributes are filled during searching from database
|
||||||
|
|
||||||
|
/** @param {int} size of the content in bytes */
|
||||||
|
this.contentSize = null;
|
||||||
|
/** @param {int} size of the content and note revision contents in bytes */
|
||||||
|
this.noteSize = null;
|
||||||
|
/** @param {int} number of note revisions for this note */
|
||||||
|
this.revisionCount = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(row) {
|
update(row) {
|
||||||
|
@ -22,7 +22,10 @@ const PROP_MAPPING = {
|
|||||||
"childrencount": "childrenCount",
|
"childrencount": "childrenCount",
|
||||||
"attributecount": "attributeCount",
|
"attributecount": "attributeCount",
|
||||||
"labelcount": "labelCount",
|
"labelcount": "labelCount",
|
||||||
"relationcount": "relationCount"
|
"relationcount": "relationCount",
|
||||||
|
"contentsize": "contentSize",
|
||||||
|
"notesize": "noteSize",
|
||||||
|
"revisioncount": "revisionCount"
|
||||||
};
|
};
|
||||||
|
|
||||||
class PropertyComparisonExp extends Expression {
|
class PropertyComparisonExp extends Expression {
|
||||||
@ -30,11 +33,15 @@ class PropertyComparisonExp extends Expression {
|
|||||||
return name in PROP_MAPPING;
|
return name in PROP_MAPPING;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(propertyName, comparator) {
|
constructor(searchContext, propertyName, comparator) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.propertyName = PROP_MAPPING[propertyName];
|
this.propertyName = PROP_MAPPING[propertyName];
|
||||||
this.comparator = comparator;
|
this.comparator = comparator;
|
||||||
|
|
||||||
|
if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyName)) {
|
||||||
|
searchContext.dbLoadNeeded = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(inputNoteSet, executionContext) {
|
execute(inputNoteSet, executionContext) {
|
||||||
|
@ -10,6 +10,9 @@ class SearchContext {
|
|||||||
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
|
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
|
||||||
this.highlightedTokens = [];
|
this.highlightedTokens = [];
|
||||||
this.originalQuery = "";
|
this.originalQuery = "";
|
||||||
|
// if true, note cache does not have (up-to-date) information needed to process the query
|
||||||
|
// and some extra data needs to be loaded before executing
|
||||||
|
this.dbLoadNeeded = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +192,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
|||||||
i += 2;
|
i += 2;
|
||||||
|
|
||||||
return new OrExp([
|
return new OrExp([
|
||||||
new PropertyComparisonExp('title', buildComparator('*=*', tokens[i].token)),
|
new PropertyComparisonExp(searchContext, 'title', buildComparator('*=*', tokens[i].token)),
|
||||||
new NoteContentProtectedFulltextExp('*=*', [tokens[i].token]),
|
new NoteContentProtectedFulltextExp('*=*', [tokens[i].token]),
|
||||||
new NoteContentUnprotectedFulltextExp('*=*', [tokens[i].token])
|
new NoteContentUnprotectedFulltextExp('*=*', [tokens[i].token])
|
||||||
]);
|
]);
|
||||||
@ -213,7 +213,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PropertyComparisonExp(propertyName, comparator);
|
return new PropertyComparisonExp(searchContext, propertyName, comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
|
searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
|
||||||
@ -307,7 +307,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueExtractor = new ValueExtractor(propertyPath);
|
const valueExtractor = new ValueExtractor(searchContext, propertyPath);
|
||||||
|
|
||||||
if (valueExtractor.validate()) {
|
if (valueExtractor.validate()) {
|
||||||
searchContext.addError(valueExtractor.validate());
|
searchContext.addError(valueExtractor.validate());
|
||||||
@ -409,7 +409,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
|||||||
|
|
||||||
function parse({fulltextTokens, expressionTokens, searchContext}) {
|
function parse({fulltextTokens, expressionTokens, searchContext}) {
|
||||||
let exp = AndExp.of([
|
let exp = AndExp.of([
|
||||||
searchContext.includeArchivedNotes ? null : new PropertyComparisonExp("isarchived", buildComparator("=", "false")),
|
searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", buildComparator("=", "false")),
|
||||||
searchContext.ancestorNoteId ? new AncestorExp(searchContext.ancestorNoteId) : null,
|
searchContext.ancestorNoteId ? new AncestorExp(searchContext.ancestorNoteId) : null,
|
||||||
getFulltext(fulltextTokens, searchContext),
|
getFulltext(fulltextTokens, searchContext),
|
||||||
getExpression(expressionTokens, searchContext)
|
getExpression(expressionTokens, searchContext)
|
||||||
@ -419,7 +419,7 @@ function parse({fulltextTokens, expressionTokens, searchContext}) {
|
|||||||
const filterExp = exp;
|
const filterExp = exp;
|
||||||
|
|
||||||
exp = new OrderByAndLimitExp([{
|
exp = new OrderByAndLimitExp([{
|
||||||
valueExtractor: new ValueExtractor(['note', searchContext.orderBy]),
|
valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]),
|
||||||
direction: searchContext.orderDirection
|
direction: searchContext.orderDirection
|
||||||
}], 0);
|
}], 0);
|
||||||
|
|
||||||
|
@ -10,6 +10,54 @@ const noteCache = require('../../note_cache/note_cache.js');
|
|||||||
const noteCacheService = require('../../note_cache/note_cache_service.js');
|
const noteCacheService = require('../../note_cache/note_cache_service.js');
|
||||||
const utils = require('../../utils.js');
|
const utils = require('../../utils.js');
|
||||||
const cls = require('../../cls.js');
|
const cls = require('../../cls.js');
|
||||||
|
const log = require('../../log.js');
|
||||||
|
|
||||||
|
function loadNeededInfoFromDatabase() {
|
||||||
|
const sql = require('../../sql.js');
|
||||||
|
|
||||||
|
for (const noteId in noteCache.notes) {
|
||||||
|
noteCache.notes[noteId].contentSize = 0;
|
||||||
|
noteCache.notes[noteId].noteSize = 0;
|
||||||
|
noteCache.notes[noteId].revisionCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteContentLengths = sql.getRows(`
|
||||||
|
SELECT
|
||||||
|
noteId,
|
||||||
|
LENGTH(content) AS length
|
||||||
|
FROM notes
|
||||||
|
JOIN note_contents USING(noteId)
|
||||||
|
WHERE notes.isDeleted = 0`);
|
||||||
|
|
||||||
|
for (const {noteId, length} of noteContentLengths) {
|
||||||
|
if (!(noteId in noteCache.notes)) {
|
||||||
|
log.error(`Note ${noteId} not found in note cache.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
noteCache.notes[noteId].contentSize = length;
|
||||||
|
noteCache.notes[noteId].noteSize = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteRevisionContentLengths = sql.getRows(`
|
||||||
|
SELECT
|
||||||
|
noteId,
|
||||||
|
LENGTH(content) AS length
|
||||||
|
FROM notes
|
||||||
|
JOIN note_revisions USING(noteId)
|
||||||
|
JOIN note_revision_contents USING(noteRevisionId)
|
||||||
|
WHERE notes.isDeleted = 0`);
|
||||||
|
|
||||||
|
for (const {noteId, length} of noteRevisionContentLengths) {
|
||||||
|
if (!(noteId in noteCache.notes)) {
|
||||||
|
log.error(`Note ${noteId} not found in note cache.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
noteCache.notes[noteId].noteSize += length;
|
||||||
|
noteCache.notes[noteId].revisionCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Expression} expression
|
* @param {Expression} expression
|
||||||
@ -22,6 +70,10 @@ function findNotesWithExpression(expression, searchContext) {
|
|||||||
? hoistedNote.subtreeNotes
|
? hoistedNote.subtreeNotes
|
||||||
: Object.values(noteCache.notes);
|
: Object.values(noteCache.notes);
|
||||||
|
|
||||||
|
if (searchContext.dbLoadNeeded) {
|
||||||
|
loadNeededInfoFromDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
// in the process of loading data sometimes we create "skeleton" note instances which are expected to be filled later
|
// in the process of loading data sometimes we create "skeleton" note instances which are expected to be filled later
|
||||||
// in case of inconsistent data this might not work and search will then crash on these
|
// in case of inconsistent data this might not work and search will then crash on these
|
||||||
allNotes = allNotes.filter(note => note.type !== undefined);
|
allNotes = allNotes.filter(note => note.type !== undefined);
|
||||||
@ -84,10 +136,7 @@ function parseQueryToExpression(query, searchContext) {
|
|||||||
* @return {SearchResult[]}
|
* @return {SearchResult[]}
|
||||||
*/
|
*/
|
||||||
function findNotesWithQuery(query, searchContext) {
|
function findNotesWithQuery(query, searchContext) {
|
||||||
if (!query.trim().length) {
|
query = query || "";
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
searchContext.originalQuery = query;
|
searchContext.originalQuery = query;
|
||||||
|
|
||||||
const expression = parseQueryToExpression(query, searchContext);
|
const expression = parseQueryToExpression(query, searchContext);
|
||||||
|
@ -19,18 +19,25 @@ const PROP_MAPPING = {
|
|||||||
"childrencount": "childrenCount",
|
"childrencount": "childrenCount",
|
||||||
"attributecount": "attributeCount",
|
"attributecount": "attributeCount",
|
||||||
"labelcount": "labelCount",
|
"labelcount": "labelCount",
|
||||||
"relationcount": "relationCount"
|
"relationcount": "relationCount",
|
||||||
|
"contentsize": "contentSize",
|
||||||
|
"notesize": "noteSize",
|
||||||
|
"revisioncount": "revisionCount"
|
||||||
};
|
};
|
||||||
|
|
||||||
class ValueExtractor {
|
class ValueExtractor {
|
||||||
constructor(propertyPath) {
|
constructor(searchContext, propertyPath) {
|
||||||
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
|
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
|
||||||
|
|
||||||
if (this.propertyPath[0].startsWith('#')) {
|
if (this.propertyPath[0].startsWith('#')) {
|
||||||
this.propertyPath = ['note', 'labels', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
this.propertyPath = ['note', 'labels', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
||||||
}
|
}
|
||||||
else if (this.propertyPath[0].startsWith('~')) {
|
else if (this.propertyPath[0].startsWith('~')) {
|
||||||
this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) {
|
||||||
|
searchContext.dbLoadNeeded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user