Possible to copy paste search results beyond just links?

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?

Some links here:

And a possible method using Templater and Dataview (I haven’t tried it yet, but plan to):

1 Like

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 :frowning:

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]*?string2sigh

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

have fun, guys :slight_smile:



credits to original dataview script by @holroy:

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

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.