1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Chore: Add types to search engine

This commit is contained in:
Laurent Cozic 2023-04-23 10:03:26 +01:00
parent af91fd99cc
commit 357a3e2e7b
6 changed files with 72 additions and 20 deletions

View File

@ -711,6 +711,7 @@ packages/lib/services/searchengine/SearchFilter.test.js
packages/lib/services/searchengine/filterParser.js packages/lib/services/searchengine/filterParser.js
packages/lib/services/searchengine/filterParser.test.js packages/lib/services/searchengine/filterParser.test.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.js packages/lib/services/searchengine/gotoAnythingStyleQuery.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.test.js
packages/lib/services/searchengine/queryBuilder.js packages/lib/services/searchengine/queryBuilder.js
packages/lib/services/share/ShareService.js packages/lib/services/share/ShareService.js
packages/lib/services/share/ShareService.test.js packages/lib/services/share/ShareService.test.js

1
.gitignore vendored
View File

@ -698,6 +698,7 @@ packages/lib/services/searchengine/SearchFilter.test.js
packages/lib/services/searchengine/filterParser.js packages/lib/services/searchengine/filterParser.js
packages/lib/services/searchengine/filterParser.test.js packages/lib/services/searchengine/filterParser.test.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.js packages/lib/services/searchengine/gotoAnythingStyleQuery.js
packages/lib/services/searchengine/gotoAnythingStyleQuery.test.js
packages/lib/services/searchengine/queryBuilder.js packages/lib/services/searchengine/queryBuilder.js
packages/lib/services/share/ShareService.js packages/lib/services/share/ShareService.js
packages/lib/services/share/ShareService.test.js packages/lib/services/share/ShareService.test.js

View File

@ -431,6 +431,7 @@ describe('services_SearchEngine', () => {
['title:abcd efgh', { _: ['efgh'], title: ['abcd'] }], ['title:abcd efgh', { _: ['efgh'], title: ['abcd'] }],
['title:abcd', { title: ['abcd'] }], ['title:abcd', { title: ['abcd'] }],
['"abcd efgh"', { _: ['abcd efgh'] }], ['"abcd efgh"', { _: ['abcd efgh'] }],
['"abcd efgh" ijkl', { _: ['abcd efgh', 'ijkl'] }],
['title:abcd title:efgh', { title: ['abcd', 'efgh'] }], ['title:abcd title:efgh', { title: ['abcd', 'efgh'] }],
]; ];

View File

@ -5,20 +5,32 @@ import Note from '../../models/Note';
import BaseModel from '../../BaseModel'; import BaseModel from '../../BaseModel';
import ItemChangeUtils from '../ItemChangeUtils'; import ItemChangeUtils from '../ItemChangeUtils';
import shim from '../../shim'; import shim from '../../shim';
import filterParser from './filterParser'; import filterParser, { Term } from './filterParser';
import queryBuilder from './queryBuilder'; import queryBuilder from './queryBuilder';
import { ItemChangeEntity, NoteEntity } from '../database/types'; import { ItemChangeEntity, NoteEntity } from '../database/types';
const { sprintf } = require('sprintf-js'); const { sprintf } = require('sprintf-js');
const { pregQuote, scriptType, removeDiacritics } = require('../../string-utils.js'); const { pregQuote, scriptType, removeDiacritics } = require('../../string-utils.js');
enum SearchType {
Auto = 'auto',
Basic = 'basic',
Nonlatin = 'nonlatin',
Fts = 'fts',
}
interface SearchOptions {
searchType: SearchType;
fuzzy: boolean;
}
export default class SearchEngine { export default class SearchEngine {
public static instance_: SearchEngine = null; public static instance_: SearchEngine = null;
public static relevantFields = 'id, title, body, user_created_time, user_updated_time, is_todo, todo_completed, todo_due, parent_id, latitude, longitude, altitude, source_url'; public static relevantFields = 'id, title, body, user_created_time, user_updated_time, is_todo, todo_completed, todo_due, parent_id, latitude, longitude, altitude, source_url';
public static SEARCH_TYPE_AUTO = 'auto'; public static SEARCH_TYPE_AUTO = SearchType.Auto;
public static SEARCH_TYPE_BASIC = 'basic'; public static SEARCH_TYPE_BASIC = SearchType.Basic;
public static SEARCH_TYPE_NONLATIN_SCRIPT = 'nonlatin'; public static SEARCH_TYPE_NONLATIN_SCRIPT = SearchType.Nonlatin;
public static SEARCH_TYPE_FTS = 'fts'; public static SEARCH_TYPE_FTS = SearchType.Fts;
public dispatch: Function = (_o: any) => {}; public dispatch: Function = (_o: any) => {};
private logger_ = new Logger(); private logger_ = new Logger();
@ -417,7 +429,7 @@ export default class SearchEngine {
const trimQuotes = (str: string) => str.startsWith('"') ? str.substr(1, str.length - 2) : str; const trimQuotes = (str: string) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
let allTerms: any[] = []; let allTerms: Term[] = [];
try { try {
allTerms = filterParser(query); allTerms = filterParser(query);
@ -429,7 +441,20 @@ export default class SearchEngine {
const titleTerms = allTerms.filter(x => x.name === 'title' && !x.negated).map(x => trimQuotes(x.value)); const titleTerms = allTerms.filter(x => x.name === 'title' && !x.negated).map(x => trimQuotes(x.value));
const bodyTerms = allTerms.filter(x => x.name === 'body' && !x.negated).map(x => trimQuotes(x.value)); const bodyTerms = allTerms.filter(x => x.name === 'body' && !x.negated).map(x => trimQuotes(x.value));
const terms: any = { _: textTerms, 'title': titleTerms, 'body': bodyTerms }; interface ComplexTerm {
type: 'regex' | 'text';
value: string;
scriptType: any;
valueRegex?: RegExp;
}
interface Terms {
_: (string | ComplexTerm)[];
title: (string | ComplexTerm)[];
body: (string | ComplexTerm)[];
}
const terms: Terms = { _: textTerms, 'title': titleTerms, 'body': bodyTerms };
// Filter terms: // Filter terms:
// - Convert wildcards to regex // - Convert wildcards to regex
@ -438,7 +463,9 @@ export default class SearchEngine {
let termCount = 0; let termCount = 0;
const keys = []; const keys = [];
for (const col in terms) { for (const col2 in terms) {
const col = col2 as '_' | 'title' | 'body';
if (!terms.hasOwnProperty(col)) continue; if (!terms.hasOwnProperty(col)) continue;
if (!terms[col].length) { if (!terms[col].length) {
@ -447,7 +474,7 @@ export default class SearchEngine {
} }
for (let i = terms[col].length - 1; i >= 0; i--) { for (let i = terms[col].length - 1; i >= 0; i--) {
const term = terms[col][i]; const term = terms[col][i] as string;
// SQlLite FTS doesn't allow "*" queries and neither shall we // SQlLite FTS doesn't allow "*" queries and neither shall we
if (term === '*') { if (term === '*') {
@ -468,12 +495,16 @@ export default class SearchEngine {
} }
// //
// The object "allTerms" is used for query construction purposes (this contains all the filter terms) // The object "allTerms" is used for query construction purposes (this
// Since this is used for the FTS match query, we need to normalize text, title and body terms. // contains all the filter terms) Since this is used for the FTS match
// Note, we're not normalizing terms like tag because these are matched using SQL LIKE operator and so we must preserve their diacritics. // query, we need to normalize text, title and body terms. Note, we're
// not normalizing terms like tag because these are matched using SQL
// LIKE operator and so we must preserve their diacritics.
// //
// The object "terms" only include text, title, body terms and is used for highlighting. // The object "terms" only include text, title, body terms and is used
// By not normalizing the text, title, body in "terms", highlighting still works correctly for words with diacritics. // for highlighting. By not normalizing the text, title, body in
// "terms", highlighting still works correctly for words with
// diacritics.
// //
allTerms = allTerms.map(x => { allTerms = allTerms.map(x => {
@ -521,9 +552,9 @@ export default class SearchEngine {
const searchOptions: any = {}; const searchOptions: any = {};
for (const key of parsedQuery.keys) { for (const key of parsedQuery.keys) {
if (parsedQuery.terms[key].length === 0) continue; if ((parsedQuery.terms as any)[key].length === 0) continue;
const term = parsedQuery.terms[key].map((x: any) => x.value).join(' '); const term = (parsedQuery.terms as any)[key].map((x: Term) => x.value).join(' ');
if (key === '_') searchOptions.anywherePattern = `*${term}*`; if (key === '_') searchOptions.anywherePattern = `*${term}*`;
if (key === 'title') searchOptions.titlePattern = `*${term}*`; if (key === 'title') searchOptions.titlePattern = `*${term}*`;
if (key === 'body') searchOptions.bodyPattern = `*${term}*`; if (key === 'body') searchOptions.bodyPattern = `*${term}*`;
@ -561,12 +592,13 @@ export default class SearchEngine {
return SearchEngine.SEARCH_TYPE_FTS; return SearchEngine.SEARCH_TYPE_FTS;
} }
public async search(searchString: string, options: any = null) { public async search(searchString: string, options: SearchOptions = null) {
if (!searchString) return []; if (!searchString) return [];
options = Object.assign({}, { options = {
searchType: SearchEngine.SEARCH_TYPE_AUTO, searchType: SearchEngine.SEARCH_TYPE_AUTO,
}, options); ...options,
};
const searchType = this.determineSearchType_(searchString, options.searchType); const searchType = this.determineSearchType_(searchString, options.searchType);
const parsedQuery = await this.parseQuery(searchString); const parsedQuery = await this.parseQuery(searchString);

View File

@ -1,5 +1,5 @@
interface Term { export interface Term {
name: string; name: string;
value: string; value: string;
negated: boolean; negated: boolean;

View File

@ -0,0 +1,17 @@
import gotoAnythingStyleQuery from './gotoAnythingStyleQuery';
describe('searchengine/gotoAnythingStyleQuery', () => {
it('should prepare queries', () => {
const testCases: [string, string][] = [
['hello', 'hello*'],
['hello welc', 'hello* welc*'],
];
for (const [input, expected] of testCases) {
const actual = gotoAnythingStyleQuery(input);
expect(actual).toBe(expected);
}
});
});