Hello, I’m curious to learn if I am missing something or is there a plug-in somewhere, i’ve looked and looked but I cannot find anything close to what I am seeking.
You know how if you search something it will give you back the results and then you can copy paste the results into a blank document but then all it gives you is a list of the links?
Is there a way instead where, let’s say I search a word and it gives me back seven different files where that word is found, instead of copy pasting the links and then me manually going into each one and then having to find where that word is again and then possibly extracting out that section for a side reason, sidestepping all this manual labor and being able to just copy paste over the actual complete section where that word is found, per document, into a new document?
Thanks for replying, The workaround section from the one example you provided “Alternative, you could click the ... menu option, “Copy search results”, [[ with ![[ so that the files are embedded in the note and the content is transcluded” worked like a charm, thank you so much!
Actually, I just realized there’s a problem. I wanted this so I could basically just copy paste over every single section that included the particular topic I was searching for in the search box, but when I do the workaround, it basically opens up the entire note from which the One Singular word simply is inside of, meaning, it’s like, merging 100% of entire notes when all I wanted it to do was represent that particular section / blurb / block/Headersworth of Words only. Le’ sigh
I’m trying to be able to copy paste over one single topic from my entire vault scattered across all my files, into Google Gemini Pro 1.5 for it to discuss that isolated topic only.
you can use obsidian query language plugin with templater as well
what you need:
the two plugins mentioned
optionally installed:
force note view mode plugin (needed for table to be seen in live preview)
meta bind plugin (needed to fire script with button)
so the drill:
create an empty file
with contents:
---
obsidianUIMode: source
obsidianEditingMode: live
---
```meta-bind-button
style: primary
label: Press to fire OQL query
id: templater-obsidian-OQL-query
action:
type: command
command: templater-obsidian:<path to and name of templater file you save the script below>.md
```
---
for example: command: templater-obsidian:templater/OQL query to print results to end of current file.md
no quotes are needed as you see
if you set up the empty file with these contents, with each subsequent query, the table results from last query will be automatically deleted so no need to CTRL+Z in here
you create and save the following script to filename given in the command above:
<%*
// Ensure that the 'obsidian-query-language' plugin is available
if ('obsidian-query-language' in this.app.plugins.plugins) {
const oql = this.app.plugins.plugins['obsidian-query-language'];
// Get user input for the query
const userQuery = await tp.system.prompt("Enter search terms");
if (!userQuery) return;
// Convert the query into extended search format
const extendedQuery = userQuery.split(' ').map(word => `'${word}`).join(' ');
// Setup and execute search with extended search enabled
const options = { useExtendedSearch: true };
const searchResults = await oql.search(extendedQuery, options);
// Filter away files starting with the query
const filteredList = searchResults
.filter(t => !t.path.split("/").pop().startsWith(extendedQuery.split(' ')[0].substring(1)))
.map(t => t.path);
// Define a function to extract a snippet of content with limited context
function extractContentSnippet(content, searchTerm) {
if (!content) return "";
if (searchTerm.includes('_image')) {
const regex = /(!\[\[.+_image[0-9]{1,3}\.(jpg|jpeg|gif|png)(.*?)\]\])/gi;
const matches = [...content.matchAll(regex)];
if (matches.length === 0) return "";
return matches.map(match => match[0]).join(' ');
}
const contextRadius = 120; // Number of characters before and after the match
const regex = new RegExp(searchTerm, 'gi');
const matches = [...content.matchAll(regex)];
if (matches.length === 0) return "";
// Take the first match for simplicity
const match = matches[0];
const matchIndex = match.index;
const start = Math.max(0, matchIndex - contextRadius);
const end = Math.min(content.length, matchIndex + match[0].length + contextRadius);
let snippet = content.slice(start, end);
// Highlight the match if not containing "_image"
if (!searchTerm.includes('_image')) {
snippet = snippet.replace(new RegExp(searchTerm, 'gi'), '==$&==');
}
return snippet;
}
// Process data to get context
const processedData = [];
for (const path of filteredList) {
const content = await this.app.vault.read(this.app.vault.getAbstractFileByPath(path));
const paragraphs = content.split(/\n\s*\n/);
const fileName = `[[${path.split('/').pop().replace('.md', '')}]]`;
const snippets = paragraphs
.filter(paragraph => new RegExp(userQuery, 'i').test(paragraph))
.map(paragraph => extractContentSnippet(paragraph, userQuery));
if (snippets.length > 0) {
processedData.push({
link: fileName,
content: snippets.join('<br><br>') // Separate each processed match with two line breaks
});
}
}
// Construct a markdown table
let markdownTable = `| Source File | Context |\n| --- | --- |\n`;
processedData.forEach(p => {
markdownTable += `| ${p.link} | ${p.content.replace(/\n/g, ' ').replace(/\|/g, '\\|')} |\n`;
});
const currentFile = app.workspace.getActiveFile();
if (currentFile) {
const currentFileName = currentFile.name;
if (userQuery === "_image") {
// Limit search to the current file and append results
const content = await this.app.vault.read(currentFile);
const paragraphs = content.split(/\n\s*\n/);
const fileName = `[[${currentFileName.replace('.md', '')}]]`;
const snippets = paragraphs
.filter(paragraph => new RegExp(userQuery, 'i').test(paragraph))
.map(paragraph => extractContentSnippet(paragraph, userQuery));
let markdownTableImage = `| Source File | Context |\n| --- | --- |\n`;
if (snippets.length > 0) {
markdownTableImage += `| ${fileName} | ${snippets.join('<br><br>').replace(/\n/g, ' ').replace(/\|/g, '\\|')} |\n`;
}
// Switch to Live Preview if user fired script in Source Mode to able to see the generated table
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)
// Append to the current file without deleting existing content
await this.app.vault.modify(currentFile, content + "\n\n\n" + markdownTableImage);
// Take me to the generated table -- Move the cursor to the last line
this.app.workspace.activeLeaf.view.editor.setCursor({ line: 99999, ch: 0 });
} else {
// Append the markdown table to the current note
let content = await this.app.vault.read(currentFile);
// Remove everything after the third "---\n\n"
content = content.replace(/(---[\s\S]*?[.]md\n[`]{3}\n\n)(---\n\n\n)([\s\S]*)$/, '$1$2');
await this.app.vault.modify(currentFile, content + "\n" + markdownTable);
}
}
// Notify the user that the table has been appended
new Notice(`Table of files containing "${userQuery}" has been added to the end of the current note.`);
} else {
new Notice('The obsidian-query-language plugin is not installed and enabled.');
}
_%>
multi-string search is supported (e.g. chicken crossed the road) and you can use OR operator like so: string1|string2 for either/or search functionality
using this script is beneficial for following reasons:
instant! results
copiable content from table
context radius is set to 120 currently – you can look for this number and increase it to give more context where search term was found
personally, I don’t like long paragraphs printed into the table
table results come in the order of filename contains first and fuzzy matched equivalent of filename(s) first
with large vaults OQL can take a while to initialize on startup but may be quicker than e.g. omnisearch plugin
if script throws some error, go to command palette and rebuild OQL search index
the script currently does way more for my own use case: it can print images into table of current (any) markdown file – if your image embed references differ from mine, you cannot use this of course
my images are like this in all my files: parentmarkdownfilename_image1,png, parentmarkdownfilename_image99.jpg, etc.
disadvantage compared to the script using dataview linked above: the OQL plugin uses fuse.js which cannot be used with regex search…so no way to match ranges as you can do with regex, such as string1.*?string2 or string1[\s\S]*?string2… sigh
advantage over that script: can be used with large vaults on mobile as well as dataview may not work on mobile…and: results are instant (only the table building will take some time)
p.s.
the new chatgpt4o model is pretty handy with templater scripts and with proper explanatory prompts you can tailor script to handle your own images and other needs
upgrade on above script with the following functionality:
image query works (possibly) for everyone
if there’s selected text before firing script, it will skip filecache-enabled file picker and user input and print results into current file
put multi-string non-image search results onto clipboard as well
<%*
// Ensure that the 'obsidian-query-language' plugin is available
if ('obsidian-query-language' in this.app.plugins.plugins) {
const oql = this.app.plugins.plugins['obsidian-query-language'];
// Get the current selection in the editor
const editor = app.workspace.activeLeaf.view.editor;
const selection = editor.getSelection().trim();
// Determine the user query
let userQuery;
if (selection) {
userQuery = selection;
} else {
// Get the list of files with aliases for the suggester
const files = app.vault.getMarkdownFiles().map(file => {
const fileCache = app.metadataCache.getFileCache(file);
file.display = file.basename;
if (fileCache?.frontmatter?.aliases) {
if (Array.isArray(fileCache.frontmatter.aliases)) {
file.display = `${file.basename}\n${fileCache.frontmatter.aliases.join(", ")}`;
} else if (typeof fileCache.frontmatter.aliases === 'string') {
file.display = `${file.basename}\n${fileCache.frontmatter.aliases}`;
}
}
return file;
});
// console.log("Files for suggester loaded", files);
// Use the suggester to select a file, if a file is selected, use its basename as a starting point
const selectedItem = await tp.system.suggester(files.map(x => x.display), files, false, 'Start typing any note name for image search...', 10);
// console.log("Selected item", selectedItem);
const initialInput = selectedItem ? `${selectedItem.basename}_image` : "_image";
// Get user input for the query with the initial input from the suggester or default value
userQuery = await tp.system.prompt("Enter search terms (use OR operator like so: `string1|string2` for either/or search functionality; leave _image suffix for image search):", initialInput, initialInput);
if (!userQuery) return;
// console.log("User query", userQuery);
}
// Check if the query includes '_image'
if (userQuery.includes('_image')) {
// console.log("Handling image query");
// Extract the file name before '_image'
const fileName = userQuery.split('_image')[0];
// Get the file path
const files = app.vault.getMarkdownFiles();
let filePath = '';
for (const file of files) {
if (file.path.endsWith(`${fileName}.md`)) {
filePath = file.path;
break;
}
}
if (!filePath) {
// console.error("File not found for image query");
return;
}
// console.log("File path for image query", filePath);
// Read the file content
const fileContent = await app.vault.read(app.vault.getAbstractFileByPath(filePath));
// console.log("File content for image query", fileContent);
// Use regex to extract image names, including modifiers if any
const regex = /!\[\[(.*?)(\.)(png|jpg|jpeg|gif|webp|bmp|svg)(.*?)\]\]|\[.*?\]\((<.*?>)?(.*?)(\.)(png|jpg|jpeg|gif|webp|bmp|svg)(.*?)\)/gi;
const matches = [...fileContent.matchAll(regex)];
// console.log("Image matches", matches);
// Process matches and push to the table
let markdownTableImage = `| Image | Embedded Image |\r\n| --- | --- |\r\n`;
let markdownImageCounter = 1;
matches.forEach(match => {
if (match[1]) {
// Wikilink style
const imageName = `${match[1]}.${match[3]}`;
const cleanedImageName = imageName.replace(/\|.+|(\|\d{2,4})/g, '');
const embeddedImage = `![[${cleanedImageName}]]`;
markdownTableImage += `| [[${cleanedImageName}]] | ${embeddedImage} |\r\n`;
} else if (match[5] || match[6]) {
// Markdown style
const prefix = match[5] ? match[5] : "";
const imageName = `${match[6]}.${match[8]}`;
const suffix = match[9] ? match[9] : "";
const link = `${prefix}${imageName}${suffix}`;
const cleanedLink = link.replace(/\|.+|(\|\d{2,4})/g, '').replace(/\|/g, '\\|');
const embeddedImage = `![image${markdownImageCounter}](${cleanedLink})`;
markdownTableImage += `| [image${markdownImageCounter}](${cleanedLink}) | ${embeddedImage} |\r\n`;
markdownImageCounter++;
}
});
if (matches.length > 0) {
// Append image results to the current file
const currentFile = app.workspace.getActiveFile();
let content = await this.app.vault.read(currentFile);
await this.app.vault.modify(currentFile, content + "\n\n" + markdownTableImage);
}
} else {
// Convert the query into extended search format for non-image searches
const extendedQuery = userQuery.split(',').map(part => part.trim().split(' ').map(word => `'${word}`).join(' ')).join(' | ');
// Setup and execute search with extended search enabled
const options = { useExtendedSearch: true };
const searchResults = await oql.search(extendedQuery, options);
// Filter away files starting with the query
const filteredList = searchResults
.filter(t => !t.path.split("/").pop().startsWith(extendedQuery.split(' ')[0].substring(1)))
.map(t => t.path);
// Define a function to extract a snippet of content with limited context
function extractContentSnippet(content, searchTerm) {
if (!content) return "";
const contextRadius = 120; // Number of characters before and after the match
const regex = new RegExp(searchTerm, 'gi');
const matches = [...content.matchAll(regex)];
if (matches.length === 0) return "";
// Take the first match for simplicity
const match = matches[0];
const matchIndex = match.index;
const start = Math.max(0, matchIndex - contextRadius);
const end = Math.min(content.length, matchIndex + match[0].length + contextRadius);
let snippet = content.slice(start, end);
// Highlight the match
snippet = snippet.replace(new RegExp(searchTerm, 'gi'), '==$&==');
return snippet;
}
// Process data to get context
const processedData = [];
for (const path of filteredList) {
const content = await this.app.vault.read(this.app.vault.getAbstractFileByPath(path));
const paragraphs = content.split(/\n\s*\n/);
const fileName = path.split('/').pop().replace('.md', '');
const snippets = paragraphs
.filter(paragraph => new RegExp(userQuery, 'i').test(paragraph))
.map(paragraph => extractContentSnippet(paragraph, userQuery));
if (snippets.length > 0) {
const dataObject = {
link: `[[${fileName}]]`,
content: snippets.join('<br><br>') // Separate each processed match with two line breaks
};
processedData.push(dataObject);
}
}
// Construct a markdown table for general results
let markdownTable = `| Source File | Results |\r\n| --- | --- |\r\n`;
processedData.forEach(p => {
markdownTable += `| ${p.link} | ${p.content.replace(/\r?\n/g, ' ').replace(/\|/g, '\\|')} |\r\n`;
});
if (processedData.length > 0) {
// Append general results to the current file
const currentFile = app.workspace.getActiveFile();
let content = await this.app.vault.read(currentFile);
await this.app.vault.modify(currentFile, content + "\n\n" + markdownTable);
// Copy the markdown table to the clipboard
await navigator.clipboard.writeText(markdownTable);
}
}
// Take me to the generated table -- Move the cursor to the last line
this.app.workspace.activeLeaf.view.editor.setCursor({ line: 99999, ch: 0 });
// Notify the user that the table has been appended
new Notice(`Table of files containing "${userQuery}" has been added to the end of the current note and copied to the clipboard.`);
} else {
new Notice('The obsidian-query-language plugin is not installed and enabled.');
}
_%>
anyone wondering why bother with this, why would you or who would need this? what are the benefits?
great for mobile use (omnisearch plugin quits obsidian on big vaults, oql plugin doesn’t)
no dataview installation needed (again, that plugin may not work on large vaults on mobile)
obsidian core search becomes slower over time, this - while having its own limitations - has instant results
first obsidian core search in session is much faster
Image plugins don’t work for mobile… you have no way to take in all your Image files of a longer note
you can copy search results from printed table (prolly the only benefit for desktop usage as well)
if you want images printed into table, keep _image suffix in query box, which is automatically appended to filename
as i said, user can change context radius of 120 in the script if they want
16.07.24. edited script to make table rendering correct for windows os as well