Skip to content

Commit

Permalink
feat: add search commands for lexical in-vault search
Browse files Browse the repository at this point in the history
  • Loading branch information
yan42685 committed Oct 26, 2024
1 parent e052a19 commit d2d20a0
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 29 deletions.
10 changes: 9 additions & 1 deletion README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@

- [x] 搜索选中文本
- [x] 自动复制选中的结果文本
- [ ] 记住上次查询文本
- [x] 搜索指令
<details><summary>详情</summary>
在搜索框输入以下指令可以临时改变搜索选项,优先级高于设置页,指令只能出现在开头且需要用空格将它与搜索文本隔开。可以任意组合指令,比如`/ap/np/nf something` 后出现的指令会覆盖前面的同类指令,即 /np 会覆盖 /ap

/ap allow prefix matching
/np no prefix matching
/af allow fuzziness
/nf no fuzziness
</details>

### 集成其他插件

Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@

- [x] Search from selection
- [x] Automatically copy result text on selection
- [ ] Remember last query text
- [x] Search commands (in-vault lexical search only)
<details><summary>Details</summary>
You can temporarily change the search options by entering the following commands in the search box. These commands take priority over the settings in the settings tab. The commands must appear at the beginning of the input and should be separated from the search text by a space. You can combine the commands in any way, for example, "/ap/np/nf something". The commands that appear later will override previous commands of the same type; for instance, /np will override /ap.

/ap allow prefix matching
/np no prefix matching
/af allow fuzziness
/nf no fuzziness
</details>


### Integrate with other plugins

Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Clever Search",
"author": "Alex Clifton",
"description": "Helping you quickly locate the notes in your mind in the easiest way, without the need for complex search syntax to find relevant content.",
"version": "0.2.11",
"version": "0.2.12",
"minAppVersion": "0.15.0",
"fundingUrl": "https://www.buymeacoffee.com/alexclifton",
"isDesktopOnly": true
Expand Down
4 changes: 2 additions & 2 deletions src/globals/plugin-setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class OuterSetting {
logLevel: LogLevel;
isCaseSensitive: boolean;
isPrefixMatch: boolean;
isCharacterFuzzyAllowed: boolean;
isFuzzy: boolean;
enableStopWordsEn: boolean;
enableChinesePatch: boolean;
enableStopWordsZh: boolean;
Expand All @@ -30,7 +30,7 @@ export const DEFAULT_OUTER_SETTING: OuterSetting = {
logLevel: isDevEnvironment ? "trace" : "info",
isCaseSensitive: false,
isPrefixMatch: true,
isCharacterFuzzyAllowed: true,
isFuzzy: true,
enableStopWordsEn: true,
// TODO: 繁体中文
enableChinesePatch: isChineseUser ? true : false,
Expand Down
4 changes: 2 additions & 2 deletions src/services/obsidian/setting-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ class GeneralTab extends PluginSettingTab {
.setName(t("Character fuzzy allowed"))
.addToggle((t) =>
t
.setValue(this.setting.isCharacterFuzzyAllowed)
.onChange((v) => (this.setting.isCharacterFuzzyAllowed = v)),
.setValue(this.setting.isFuzzy)
.onChange((v) => (this.setting.isFuzzy = v)),
);

new Setting(containerEl)
Expand Down
100 changes: 84 additions & 16 deletions src/services/search/lexical-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { logger } from "src/utils/logger";
import { getInstance, monitorDecorator } from "src/utils/my-lib";
import { singleton } from "tsyringe";
import { OuterSetting, innerSetting } from "../../globals/plugin-setting";
import { Query } from "./query";
import { Tokenizer } from "./tokenizer";
import { TruncateOption, type TruncateType } from "./truncate-option";

Expand Down Expand Up @@ -107,14 +106,12 @@ export class LexicalEngine {
*/
@monitorDecorator
async searchFiles(queryText: string): Promise<MatchedFile[]> {
// const combinationMode = this.tokenizer.isLargeCharset(queryText) ? "or" : "and";
const combinationMode = "and";
// TODO: if queryText.length === 0, return empty,
// else if (length === 1 && isn't Chinese char) only search filename
const query = new Query(queryText);
const minisearchResult = this.filesIndex.search(
query.text,
this.option.getFileSearchOption(combinationMode),
this.option.getFileSearchOption(query.userOption),
);
logger.debug(`maxFileItems: ${this.outerSetting.ui.maxItemResults}`);
return minisearchResult
Expand Down Expand Up @@ -271,18 +268,18 @@ class LexicalOptions {
* - "and": Requires any single token to appear in the fields.
* - "or": Requires all tokens to appear across the fields.
*/
getFileSearchOption(combinationMode: "and" | "or"): SearchOptions {
getFileSearchOption(userOption: UserSearchOption): SearchOptions {
return {
tokenize: this.tokenizeSearch,
// TODO: for autosuggestion, we can choose to do a prefix match only when the term is
// at the last index of the query terms
prefix: (term) =>
this.outerSetting.isPrefixMatch
userOption.isPrefixMatch
? term.length >= this.inSetting.minTermLengthForPrefixSearch
: false,
// TODO: fuzziness based on language
fuzzy: (term) =>
this.outerSetting.isCharacterFuzzyAllowed
userOption.isFuzzy
? term.length <= 3
? 0
: this.inSetting.fuzzyProportion
Expand All @@ -295,7 +292,7 @@ class LexicalOptions {
tags: this.inSetting.weightTagText,
headings: this.inSetting.weightHeading,
} as DocumentWeight,
combineWith: combinationMode,
combineWith: "and",
};
}

Expand All @@ -312,6 +309,8 @@ class LexicalOptions {
}

class LinesMatcher {
private outerSetting = getInstance(OuterSetting);
private userOption: UserSearchOption;
private lines: Line[];
private matchedTerms: string[];
private maxParsedLines: number;
Expand All @@ -326,11 +325,14 @@ class LinesMatcher {
matchedTerms: string[],
maxParsedLines: number,
) {
const query = new Query(queryText);

this.userOption = query.userOption;
this.lines = lines;
this.matchedTerms = this.filterMatchedTerms(queryTerms, matchedTerms);
this.maxParsedLines = maxParsedLines;
// TODO: use token rather than chars
const truncateLimit = TruncateOption.forType(truncateType, queryText);
const truncateLimit = TruncateOption.forType(truncateType, query.text);
this.preChars = truncateLimit.maxPreChars;
this.postChars = truncateLimit.maxPostChars;

Expand Down Expand Up @@ -412,7 +414,8 @@ class LinesMatcher {

// map each matchedTerm to a global regex
const globalRegexes = this.matchedTerms.map(
(term) => new RegExp(term, "gi"),
// (term) => new RegExp(term, "gi"),
(term) => this.generateRegExpForTerm(term),
);

const topKLinesScores = new PriorityQueue<number>(
Expand Down Expand Up @@ -465,7 +468,8 @@ class LinesMatcher {
private highlightLines(lines: Line[]): MatchedLine[] {
const termRegexMap = new Map<string, RegExp>();
for (const term of this.matchedTerms) {
termRegexMap.set(term, new RegExp(term, "gi"));
// termRegexMap.set(term, new RegExp(term, "gi"));
termRegexMap.set(term, this.generateRegExpForTerm(term));
}
return lines.map((line) => {
const positions = new Set<number>();
Expand Down Expand Up @@ -513,14 +517,78 @@ class LinesMatcher {
};
});
}

private generateRegExpForTerm(term: string): RegExp {
const flags = this.outerSetting.isCaseSensitive ? "g" : "gi";
const pattern = this.userOption.isPrefixMatch
? term
: `${term}(?![a-zA-Z])`;
return new RegExp(pattern, flags);
}
}

class UserSearchOption {
public readonly isPrefixMatch: boolean;
public readonly isCharacterFuzzyAllowed: boolean;
private outerSetting = getInstance(OuterSetting);
public isPrefixMatch: boolean;
public isFuzzy: boolean;

constructor() {
this.isPrefixMatch = this.outerSetting.isPrefixMatch;
this.isFuzzy = this.outerSetting.isFuzzy;
}
}

class Query {
private static readonly registeredCommands = new Set([
"ap", // allow prefix matching
"np", // no prefix matching
"af", // allow fuzziness
"nf", // no fuzziness
]);

constructor(isPrefixMatch: boolean, isCharacterFuzzyAllowed: boolean) {
this.isPrefixMatch = isPrefixMatch;
this.isCharacterFuzzyAllowed = isCharacterFuzzyAllowed;
// text to be searched
public text: string;
public userOption: UserSearchOption = new UserSearchOption();

constructor(queryText: string) {
this.parse(queryText);
}

private parse(queryText: string): void {
let commandPart = "";

// parse commandPart
if (queryText.startsWith("/")) {
const spaceIndex = queryText.indexOf(" ");
if (spaceIndex !== -1) {
commandPart = queryText.slice(0, spaceIndex);
this.text = queryText.slice(spaceIndex + 1);
} else {
commandPart = queryText; // no space found, treat the whole thing as command
this.text = "";
}

const commands = commandPart.split("/").filter((part) => part); // skip empty parts

for (const command of commands) {
if (command === "ap") {
this.userOption.isPrefixMatch = true;
} else if (command === "np") {
this.userOption.isPrefixMatch = false;
} else if (command === "af") {
this.userOption.isFuzzy = true;
} else if (command === "nf") {
this.userOption.isFuzzy = false;
} else {
// if an unregistered command is encountered, stop further parsing and reset the options
this.userOption = new UserSearchOption();
logger.info("invalid command: /" + command);
break;
}
}
} else {
// no leading '/', treat entire queryText as search text
this.text = queryText;
}
}
}
6 changes: 0 additions & 6 deletions src/services/search/query.ts

This file was deleted.

0 comments on commit d2d20a0

Please sign in to comment.