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}`);
|
||||
}
|
||||
|
||||
// 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
|
||||
if (note.noteId in treeCache.notes) {
|
||||
treeCache.notes[note.noteId].children = [];
|
||||
|
@ -164,6 +164,9 @@ const TPL = `
|
||||
<option value="title">Title</option>
|
||||
<option value="dateCreated">Date created</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 name="orderDirection" class="form-control w-auto d-inline">
|
||||
|
@ -15,7 +15,7 @@ async function search(note) {
|
||||
|
||||
if (searchScript) {
|
||||
searchResultNoteIds = await searchFromRelation(note, 'searchScript');
|
||||
} else if (searchString) {
|
||||
} else {
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: note.hasLabel('fastSearch'),
|
||||
ancestorNoteId: note.getRelationValue('ancestor'),
|
||||
@ -27,8 +27,6 @@ async function search(note) {
|
||||
|
||||
searchResultNoteIds = searchService.findNotesWithQuery(searchString, searchContext)
|
||||
.map(sr => sr.noteId);
|
||||
} else {
|
||||
searchResultNoteIds = [];
|
||||
}
|
||||
|
||||
// we won't return search note's own noteId
|
||||
|
@ -30,6 +30,15 @@ class Note {
|
||||
|
||||
/** @param {Note[]|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) {
|
||||
|
@ -22,7 +22,10 @@ const PROP_MAPPING = {
|
||||
"childrencount": "childrenCount",
|
||||
"attributecount": "attributeCount",
|
||||
"labelcount": "labelCount",
|
||||
"relationcount": "relationCount"
|
||||
"relationcount": "relationCount",
|
||||
"contentsize": "contentSize",
|
||||
"notesize": "noteSize",
|
||||
"revisioncount": "revisionCount"
|
||||
};
|
||||
|
||||
class PropertyComparisonExp extends Expression {
|
||||
@ -30,11 +33,15 @@ class PropertyComparisonExp extends Expression {
|
||||
return name in PROP_MAPPING;
|
||||
}
|
||||
|
||||
constructor(propertyName, comparator) {
|
||||
constructor(searchContext, propertyName, comparator) {
|
||||
super();
|
||||
|
||||
this.propertyName = PROP_MAPPING[propertyName];
|
||||
this.comparator = comparator;
|
||||
|
||||
if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyName)) {
|
||||
searchContext.dbLoadNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
execute(inputNoteSet, executionContext) {
|
||||
|
@ -10,6 +10,9 @@ class SearchContext {
|
||||
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
|
||||
this.highlightedTokens = [];
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -192,7 +192,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
i += 2;
|
||||
|
||||
return new OrExp([
|
||||
new PropertyComparisonExp('title', buildComparator('*=*', tokens[i].token)),
|
||||
new PropertyComparisonExp(searchContext, 'title', buildComparator('*=*', tokens[i].token)),
|
||||
new NoteContentProtectedFulltextExp('*=*', [tokens[i].token]),
|
||||
new NoteContentUnprotectedFulltextExp('*=*', [tokens[i].token])
|
||||
]);
|
||||
@ -213,7 +213,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new PropertyComparisonExp(propertyName, comparator);
|
||||
return new PropertyComparisonExp(searchContext, propertyName, comparator);
|
||||
}
|
||||
|
||||
searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`);
|
||||
@ -307,7 +307,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
i++;
|
||||
}
|
||||
|
||||
const valueExtractor = new ValueExtractor(propertyPath);
|
||||
const valueExtractor = new ValueExtractor(searchContext, propertyPath);
|
||||
|
||||
if (valueExtractor.validate()) {
|
||||
searchContext.addError(valueExtractor.validate());
|
||||
@ -409,7 +409,7 @@ function getExpression(tokens, searchContext, level = 0) {
|
||||
|
||||
function parse({fulltextTokens, expressionTokens, searchContext}) {
|
||||
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,
|
||||
getFulltext(fulltextTokens, searchContext),
|
||||
getExpression(expressionTokens, searchContext)
|
||||
@ -419,7 +419,7 @@ function parse({fulltextTokens, expressionTokens, searchContext}) {
|
||||
const filterExp = exp;
|
||||
|
||||
exp = new OrderByAndLimitExp([{
|
||||
valueExtractor: new ValueExtractor(['note', searchContext.orderBy]),
|
||||
valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]),
|
||||
direction: searchContext.orderDirection
|
||||
}], 0);
|
||||
|
||||
|
@ -10,6 +10,54 @@ const noteCache = require('../../note_cache/note_cache.js');
|
||||
const noteCacheService = require('../../note_cache/note_cache_service.js');
|
||||
const utils = require('../../utils.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
|
||||
@ -22,6 +70,10 @@ function findNotesWithExpression(expression, searchContext) {
|
||||
? hoistedNote.subtreeNotes
|
||||
: 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 case of inconsistent data this might not work and search will then crash on these
|
||||
allNotes = allNotes.filter(note => note.type !== undefined);
|
||||
@ -84,10 +136,7 @@ function parseQueryToExpression(query, searchContext) {
|
||||
* @return {SearchResult[]}
|
||||
*/
|
||||
function findNotesWithQuery(query, searchContext) {
|
||||
if (!query.trim().length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
query = query || "";
|
||||
searchContext.originalQuery = query;
|
||||
|
||||
const expression = parseQueryToExpression(query, searchContext);
|
||||
|
@ -19,18 +19,25 @@ const PROP_MAPPING = {
|
||||
"childrencount": "childrenCount",
|
||||
"attributecount": "attributeCount",
|
||||
"labelcount": "labelCount",
|
||||
"relationcount": "relationCount"
|
||||
"relationcount": "relationCount",
|
||||
"contentsize": "contentSize",
|
||||
"notesize": "noteSize",
|
||||
"revisioncount": "revisionCount"
|
||||
};
|
||||
|
||||
class ValueExtractor {
|
||||
constructor(propertyPath) {
|
||||
constructor(searchContext, propertyPath) {
|
||||
this.propertyPath = propertyPath.map(pathEl => pathEl.toLowerCase());
|
||||
|
||||
if (this.propertyPath[0].startsWith('#')) {
|
||||
this.propertyPath = ['note', 'labels', this.propertyPath[0].substr(1), ...this.propertyPath.slice( 1, this.propertyPath.length)];
|
||||
}
|
||||
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