Obsidian 全局搜索增强及搜索结果可视化

Obsidian 全局搜索增强及搜索结果可视化

Obsidian 全局搜索转换为高级搜索界面,灵感来自知网高级检索界面 [1]

Pasted image 20250115000433765×372 25.8 KB

功能特点

Pasted image 202501142232361920×1080 313 KB

可视化搜索条件配置,自动转换为 Obsidian 搜索语法

Pasted image 20250115001242771×354 28 KB

由 Obsidian 原生的查询,查询结果可以定位到具体的块

Pasted image 20250115001202767×490 53.1 KB

支持多条件组合搜索,可以逐步优化检索信息

搜索结果可以配合全局图谱可视化检索的图谱,可显示笔记间的关联性以及标签附件等信息

Pasted image 202501150008431845×817 183 KB

可利用书签功能保存检索结果,以及导出检索的笔记信息

Pasted image 20250115000548774×463 48.1 KB

Tip:推荐配合 better search views 使用,自带搜索结果并不支持渲染,安装这个插件后可以渲染检索结果。

实现过程

参考知网的高级检索的控件,强化 Obsidian 自带的全局搜索,设计草图以及 html 中实现。

Pasted image 202501142308481685×826 142 KB

最终合并到自带的全局搜索元素上,Obsidian 中按钮这些样式就比较难调,而且很受主题的影响,所以比原计划的要难看点。

搜索运算符类型

检索的运算符类型主要有这几种:

搜索运算符

描述

file:

查找文件名中的文本。匹配 Vault 中的任何文件。示例: file:.jpg 或 file:202209 。

path:

在文件路径中查找文本。匹配 Vault 中的任何文件。示例: path:"Daily notes/2022-07" 。

content:

在文件内容中查找文本。示例: content:"happy cat" 。

match-case:

区分大小写的匹配。示例: match-case:HappyCat 。

ignore-case:

不区分大小写的匹配。示例: ignore-case:ikea 。

tag:

在文件中查找标签。示例: tag:#work 。请记住,搜索 tag:#work 不会返回 #myjob/work 的结果。 注意:由于 tag: 忽略代码块和非 Markdown 内容中的匹配,因此它通常比 #work 的普通全文搜索更快、更准确。

line:

在同一行查找匹配项。示例: line:(mix flour) 。

block:

在同一块中查找匹配项。示例: block:(dog cat) 。注意:由于 block: 需要搜索来解析每个文件中的 Markdown 内容,因此可能会导致您的搜索词需要更长的时间才能完成。

section:

在同一部分(两个标题之间的文本)中查找匹配项。示例: section:(dog cat) 。

task:

按照块的方式在任务中查找匹配项。示例: task:call 。

task-todo:

按照区块的方式在未完成的任务中找到匹配项。示例: task-todo:call 。

task-done:

按照区块的方式在已完成的任务中找到匹配项。示例: task-done:call 。

基于 Obsidian 原生搜索运算符实现,搜索案例:

类型

使用场景

示例

文件名搜索

查找特定文件名

file:.jpg

路径搜索

定位特定目录下的文件

path:"Daily notes"

内容搜索

全文检索

content:"关键词"

标签搜索

通过标签筛选

tag:#work

任务搜索

查找待办事项

task-todo:call

脚本源码

/*

* @Author: 熊猫别熬夜

* @Date: 2025-01-15 00:13:57

* @Last Modified by: 熊猫别熬夜

* @Last Modified time: 2025-01-19 00:40:21

*/

module.exports = async () => {

(function () {

// 创建并添加样式

const styleElement = document.createElement("style");

styleElement.textContent = `

.search-params{

display: flex;

flex-flow: row wrap;

justify-content: space-between;

}

.search-form-container {

width: 100%;

padding: 10px;

margin: auto;

background-color: var(--background-primary);

button {

background-color: var(--background-primary);

border-radius: 4px;

border: 1px solid var(--background-modifier-border);

}

select, label, button {

padding: 4px;

}

/* 隐藏第一行操作符和删除按钮 */

.form-row:first-child .operator,

.form-row:first-child .remove-row {

display: none;

}

.form-row {

display: flex;

gap: 0 5px;

align-items: center;

margin-bottom: 10px;

input[type="text"] {

flex: 1;

border-width: 1px;

}

}

}

.input-group {

display: flex;

width: 100%;

align-items: center;

height: 20px;

input {

margin-right: 0px !important;

padding: 5px;

border-radius: 4px 0 0 4px;

box-shadow: none !important;

}

.icon-button {

box-shadow: none;

color: var(--text-normal);

margin-left: 0px !important;

border-left: none;

border-radius: 0 4px 4px 0;

cursor: pointer;

}

.icon-button[data-select-option=""] {

display: none;

}

}

/* 大小写和正则控件 */

.controls {

display: flex;

gap: 0 2px;

.toggle.toggle {

padding: 0px;

margin: 0px;

cursor: pointer;

display: flex;

input {

display: none;

}

}

.toggle-label {

display: flex;

align-items: center;

justify-items: center;

padding: 2px 2px;

border-radius: 4px;

}

.toggle input:checked+.toggle-label {

background: var(--background-modifier-hover);

}

}

.button-group {

display: flex;

justify-content: space-between;

button {

padding: 5px 10px;

}

}

.date-group {

width: 100%;

display: flex;

justify-content: space-between;

button {

border-width: 1px;

border-radius: 3px;

}

}

.navigation-buttons {

width: 100%;

display: flex;

justify-content: space-between;

gap: 4px;

button {

border: none;

}

.import-button,

.copy-button,

.reset-button {

flex: 1;

font-size: large;

}

.graph-button,

.search-button {

flex: 1;

color: var(--text-accent);

font-size: large;

}

.reset-button {

color: var(--text-error);

}

}

.result {

margin-top: 10px;

textarea {

width: 100%;

height: 300px;

resize: vertical;

}

}

`;

document.head.appendChild(styleElement);

// 移除已存在的 form-container

const existingContainer = document.querySelector('.search-form-container');

if (existingContainer) {

existingContainer.remove();

}

// 创建 HTML 结构

const queryControlsContainer = document.createElement("div");

queryControlsContainer.className = "search-form-container";

queryControlsContainer.innerHTML = `

`;

// 添加容器

document.body.appendChild(queryControlsContainer);

// 找到搜索容器并在其第一个子元素前插入

const searchContainer = document.querySelector('.workspace-leaf-content[data-type="search"]');

if (searchContainer) {

searchContainer.insertBefore(queryControlsContainer, searchContainer.children[0]);

}

})();

function generateIcons(options, values) {

return options.reduce((icons, option) => {

icons[option] = values[option] || '';

return icons;

}, {});

}

const options = ["all", "file", "tag", "path", "content", "line", "block", "section", "task", "task-todo", "tasks-done"];

const iconsWithValues = {

'file': '',

'tag': '',

'path': ''

};

const icons = generateIcons(options, iconsWithValues);

// !点击图标触发事件

async function handleTypeIconClick(button) {

const row = button.closest('.form-row');

const type = row.querySelector('.type').value;

const options = getOptionsByType(type);

const quickAddApi = app.plugins.plugins.quickadd.api;

const choice = await quickAddApi.suggester(options, options);

if (choice) {

const type = row.querySelector('.type').value;

if (type === 'tag') {

row.querySelector('input[type="text"]').value += ` ${choice.replace(/^#/, '')}`;

} else {

row.querySelector('input[type="text"]').value += ` "${choice}"`;

}

}

}

// 图标类型

function getOptionsByType(type) {

let options;

switch (type) {

case 'file':

options = app.vault.getFiles()

.filter(f => f.path.endsWith('.md'))

.map(f => f.basename);

options.sort();

break;

case 'tag':

options = Object.keys(app.metadataCache.getTags());

options.sort();

break;

case 'path':

options = app.vault.getAllFolders().map(f => f.path);

break;

default:

return [];

}

return options;

}

// !添加和删除按钮

function addRow(button) {

const currentRow = button.closest('.form-row');

const currentType = currentRow.querySelector('.type').value;

const currentOperator = currentRow.querySelector('.operator').value;

const newRow = currentRow.cloneNode(true);

// 只重置文本输入

newRow.querySelectorAll('input[type="text"]').forEach(input => {

input.value = '';

});

// 为新行的 radio 设置新的 name

const newName = `search-mode-${Math.random().toString(36).substr(2, 9)}`;

newRow.querySelectorAll('input[type="radio"]').forEach(radio => {

radio.name = newName;

});

// 设置 operator 的值

newRow.querySelector('.operator').value = currentOperator;

currentRow.parentNode.insertBefore(newRow, currentRow.nextSibling);

initializeRow(newRow, currentType);

}

function removeRow(button) {

const row = button.closest('.form-row');

const container = row.parentNode;

if (container.querySelectorAll('.form-row').length > 1) {

row.remove();

}

}

function initializeRow(row, option, clearInputs = false) {

if (clearInputs) {

row.querySelectorAll('input[type="text"]').forEach(input => {

input.value = '';

});

}

const typeSelect = row.querySelector('.type');

const button = row.querySelector('.icon-button');

// 使用选中的属性初始化选项

typeSelect.innerHTML = options.map(opt =>

``

).join('');

// 设置默认选项

typeSelect.value = option; // 确保选择项被设置

// 添加 change 事件监听器

typeSelect.addEventListener('change', function () {

const selectedOption = typeSelect.value;

button.setAttribute('data-select-option', icons[selectedOption]);

button.innerHTML = icons[selectedOption];

});

// 触发 change 事件以设置初始状态

typeSelect.dispatchEvent(new Event('change'));

// 添加可取消选择的单选框逻辑

const radios = row.querySelectorAll('input[type="radio"]');

const rowName = `search-mode-${Math.random().toString(36).substr(2, 9)}`; // 每行共用一个name

radios.forEach(radio => {

radio.name = rowName; // 同一行的radio使用相同name实现互斥

let lastState = false;

radio.addEventListener('click', function () {

if (this.checked && lastState) {

this.checked = false;

}

lastState = this.checked;

});

});

}

function clearDate(button) {

const container = button.parentElement;

container.querySelectorAll('input[type="date"]').forEach(input => {

input.value = '';

});

}

// 转换查询条件为 Obsidian 搜索语法

function convertToObsidianQuery(formRows, lineBreak = false) {

let query = [];

formRows.forEach(row => {

const operator = row.querySelector('.operator').value;

let type = row.querySelector('.type').value;

type = type === 'all' ? "" : `${type}:`;

const input = row.querySelector('input[type="text"]').value;

const isCaseSensitive = row.querySelector('.case-sensitive').checked;

const isRegex = row.querySelector('.regex').checked;

if (input.trim()) {

let searchTerm = input;

if (isRegex) {

searchTerm = `/${searchTerm}/`;

} else if (type == 'tag:') {

searchTerm = searchTerm.trim().split(" ").map(t => t.startsWith("#") ? t : `#${t}`).join(" ");

} else {

searchTerm = `(${searchTerm})`;

}

if (isCaseSensitive) {

searchTerm = `match-case:${searchTerm}`;

}

let queryPart = '';

switch (operator) {

case 'AND': queryPart = `(${type}${searchTerm})`; break;

case 'OR': queryPart = `${operator} (${type}${searchTerm})`; break;

case 'NOT': queryPart = `-(${type}${searchTerm})`; break;

}

query.push(queryPart);

}

});

return lineBreak ? query.join("\n") : query.join(" ");

}

// 搜索按钮点击处理函数

function executeSearch() {

const formRows = document.querySelectorAll('.form-row');

const queryValue = convertToObsidianQuery(formRows);

const searchInputs = document.querySelectorAll('.search-input-container > input');

searchInputs.forEach(searchInput => {

if (searchInput && searchInput.value !== queryValue) {

searchInput.value = queryValue;

searchInput.dispatchEvent(new Event('input', { bubbles: true }));

searchInput.dispatchEvent(new KeyboardEvent('keydown', {

key: 'Escape',

code: 'Escape',

keyCode: 27,

bubbles: true

}));

}

});

}

function openGraphView() {

app.commands.executeCommandById("graph:open");

setTimeout(() => {

const formRows = document.querySelectorAll('.form-row');

const queryValue = convertToObsidianQuery(formRows);

// 更新图谱视图的搜索框

const graphSearch = document.querySelector('.graph-control-section .search-input-container input');

if (graphSearch) {

graphSearch.value = queryValue;

graphSearch.dispatchEvent(new Event('input', { bubbles: true }));

graphSearch.dispatchEvent(new KeyboardEvent('keydown', {

key: 'Escape',

code: 'Escape',

keyCode: 27,

bubbles: true

}));

}

}, 100);

}

// 添加重置函数

function clearSearchForm(n = 2) {

const container = document.querySelector('.search-section');

if (!container) return;

const templateRow = container.querySelector('.form-row');

if (!templateRow) return;

// 清空现有内容

container.innerHTML = '';

// 添加初始行

for (let i = 0; i < n; i++) {

const newRow = templateRow.cloneNode(true);

container.appendChild(newRow);

initializeRow(newRow, options[0], true);

}

// 触发 change 事件以确保 UI 更新

container.dispatchEvent(new Event('change', { bubbles: true }));

}

// 添加导入功能

function importFromSearchBox() {

const searchInput = document.querySelector('.workspace-leaf-content[data-type="search"] .search-row input');

if (!searchInput || !searchInput.value.trim()) {

new Notice('No query to import');

return;

}

// 解析查询字符串

const query = searchInput.value.trim();

const parts = query.split(/(?<=\)) (?=[-(]|\w+:|\()/g).filter(p => p.trim());

// 清空现有行并生成对应行数

clearSearchForm(parts.length);

const container = document.querySelector('.search-section');

const templateRow = container.querySelector('.form-row');

parts.forEach((part, index) => {

let row = index === 0 ? templateRow : templateRow.cloneNode(true);

if (index > 0) {

container.appendChild(row);

initializeRow(row, 'all');

}

// 解析操作符

if (part.startsWith('-')) {

row.querySelector('.operator').value = 'NOT';

part = part.slice(1);

} else if (part.startsWith('OR ')) {

row.querySelector('.operator').value = 'OR';

part = part.slice(3);

} else {

row.querySelector('.operator').value = 'AND';

}

// 解析类型和值

let type = 'all';

let value = part.replace(/^\(|\)$/g, '');

const typeMatch = value.match(/^(file|tag|path|content|line|block|section|task|task-todo|tasks-done):/);

if (typeMatch) {

type = typeMatch[1];

value = value.slice(typeMatch[0].length);

}

// 设置类型

row.querySelector('.type').value = type;

// 处理大小写和正则

const caseSensitive = value.startsWith('match-case:');

if (caseSensitive) {

row.querySelector('.case-sensitive').checked = true;

value = value.slice(11);

}

const isRegex = value.startsWith('/') && value.endsWith('/');

if (isRegex) {

row.querySelector('.regex').checked = true;

value = value.slice(1, -1);

}

// 处理标签特殊格式

if (type === 'tag') {

value = value.replace(/#/g, '');

}

// 设置值

row.querySelector('input[type="text"]').value = value.replace(/^\(|\)$/g, '');

});

// 清空空行

const rows = container.querySelectorAll('.form-row');

rows.forEach(row => {

const input = row.querySelector('input[type="text"]');

if (!input.value.trim()) {

row.remove();

}

});

}

function copyToClipboard(extrTexts) {

const txtArea = document.createElement('textarea');

txtArea.value = extrTexts;

document.body.appendChild(txtArea);

txtArea.select();

if (document.execCommand('copy')) {

new Notice('Copied to clipboard');

} else {

new Notice('Failed to copy');

}

document.body.removeChild(txtArea);

}

function copySearchQuery() {

const formRows = document.querySelectorAll('.form-row');

const queryValue = convertToObsidianQuery(formRows, true);

const formattedQuery = `\`\`\`query\n${queryValue}\n\`\`\``;

copyToClipboard(formattedQuery);

}

// 把所有函数暴露到全局作用域

window.addRow = addRow;

window.removeRow = removeRow;

window.handleTypeIconClick = handleTypeIconClick;

window.openGraphView = openGraphView;

window.executeSearch = executeSearch;

window.clearSearchForm = clearSearchForm;

window.clearDate = clearDate;

// 绑定导入按钮事件

window.importFromSearchBox = importFromSearchBox;

window.copySearchQuery = copySearchQuery;

// 修改初始化逻辑

(function initialize() {

clearSearchForm();

})();

};

脚本配置

该脚本主要是通过 QuickAdd Macro 运行的,配置流程如下:

将脚本代码保存为 js 文件

将该 js 文件放置到 QuickAdd 设置的模版路径中

添加一个 QuickAdd Macro 动作

设置 Macro 动作,将刚刚的定义的脚本选择进来

配置一个快捷键或设置一个按钮 (通过 Commanders 或 Note toolbar 插件) 来启动该命令。

存在问题

不能实现比较复杂的分组组合语法

如果全局搜索界面关闭后控件会消失,需要重新运行该脚本。

Reference

Obsidian 官方文档:Search

PKMer_Obsidian 全局搜索功能

功能小结

image1193×412 115 KB

Pasted image 202410272328141248×446 32.3 KB ↩︎

相关推荐

音频直播基础篇(100个话题之上篇)
365bet比分网

音频直播基础篇(100个话题之上篇)

📅 01-26 👁️ 6097
《笼》字义,《笼》字的字形演变,小篆隶书楷书写法《笼》
美国银行电汇: 您需要了解的内容
365bet的官网是多少

美国银行电汇: 您需要了解的内容

📅 08-25 👁️ 423
工资超过多少要交税?你必须知道的个税起征点与税率全解析
江湖求生 | 13小时9.6分问鼎预约双榜,首款中国风武侠吃鸡手游来了!
苹果4手机下载微信方法 苹果4手机下载微信教程
365bet比分网

苹果4手机下载微信方法 苹果4手机下载微信教程

📅 07-02 👁️ 5780