First commit

This commit is contained in:
ashwin271 2025-01-10 22:13:53 +05:30
commit 3b27bc8e7f
15 changed files with 2882 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View file

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

72
README.md Normal file
View file

@ -0,0 +1,72 @@
# Obsidian Vector Search Plugin
This plugin adds semantic search capabilities to Obsidian using Ollama's embedding API. It allows you to find semantically similar notes based on content rather than just keyword matching.
## Features
- 🔍 Semantic search across your entire vault
- 🤖 Powered by Ollama's embedding model
- 📊 Configurable similarity threshold
- 🚀 Fast local search once embeddings are generated
## Prerequisites
- [Ollama](https://ollama.ai/) installed and running locally
- The `nomic-embed-text` model pulled in Ollama
## Installation
1. Clone this repo to your `.obsidian/plugins/` folder
2. Install dependencies: `npm install`
3. Build the plugin: `npm run dev`
4. Enable the plugin in Obsidian's settings
## Usage
1. **Initial Setup**
- Go to Settings > Vector Search
- Configure your Ollama URL (default: http://localhost:11434)
- Set your desired similarity threshold (0-1)
2. **Building the Index**
- Click the vector search icon in the ribbon
- Wait for all notes to be processed (progress will be shown)
3. **Searching**
- Select any text in a note
- Use the command "Find Similar Notes" (or set up a hotkey)
- View results in the popup modal
## How it Works
1. The plugin creates vector embeddings for all your markdown notes using Ollama
2. When you search, it:
- Creates an embedding for your selected text
- Uses cosine similarity to find the most similar notes
- Shows results above your configured threshold
## Development
- `npm run dev` - Start compilation in watch mode
- `npm run build` - Build the plugin
- `npm test` - Run tests
## Logs
The plugin logs all API calls and operations to the Developer Console (Ctrl+Shift+I or Cmd+Option+I on Mac). This includes:
- Embedding API requests
- Index building progress
- File processing status
- Any errors or issues
## Configuration
```json
{
"ollamaURL": "http://localhost:11434",
"searchThreshold": 0.7
}
```

49
esbuild.config.mjs Normal file
View file

@ -0,0 +1,49 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

221
main.ts Normal file
View file

@ -0,0 +1,221 @@
import {
App,
Editor,
MarkdownView,
Modal,
Notice,
Plugin,
PluginSettingTab,
Setting,
TFile
} from 'obsidian';
interface MyPluginSettings {
ollamaURL: string;
searchThreshold: number;
}
const DEFAULT_SETTINGS: MyPluginSettings = {
ollamaURL: 'http://localhost:11434',
searchThreshold: 0.8
}
export default class VectorSearchPlugin extends Plugin {
settings: MyPluginSettings;
vectorStore: Map<string, number[]> = new Map();
async onload() {
await this.loadSettings();
// Add a ribbon icon for rebuilding the vector index
this.addRibbonIcon('refresh-cw', 'Rebuild Vector Index', async () => {
await this.buildVectorIndex();
new Notice('Vector index rebuilt!');
});
// Add a command to search similar notes
this.addCommand({
id: 'search-similar-notes',
name: 'Search Similar Notes',
editorCallback: async (editor: Editor, view: MarkdownView) => {
const selection = editor.getSelection();
if (selection) {
await this.searchSimilarNotes(selection);
} else {
new Notice('Please select some text to search');
}
}
});
// Add settings tab
this.addSettingTab(new VectorSearchSettingTab(this.app, this));
}
async onunload() {
// Clean up resources if needed
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
// Function to get embeddings from Ollama
async getEmbedding(text: string): Promise<number[]> {
try {
console.log(`[Vector Search] Requesting embedding for text of length ${text.length}`);
const response = await fetch(`${this.settings.ollamaURL}/api/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'nomic-embed-text',
prompt: text
})
});
const data = await response.json();
console.log(`[Vector Search] Successfully received embedding of length ${data.embedding.length}`);
return data.embedding;
} catch (error) {
console.error('[Vector Search] Error getting embedding:', error);
new Notice('Error getting embedding from Ollama');
return [];
}
}
// Calculate cosine similarity between two vectors
cosineSimilarity(vec1: number[], vec2: number[]): number {
const dotProduct = vec1.reduce((acc, val, i) => acc + val * vec2[i], 0);
const mag1 = Math.sqrt(vec1.reduce((acc, val) => acc + val * val, 0));
const mag2 = Math.sqrt(vec2.reduce((acc, val) => acc + val * val, 0));
return dotProduct / (mag1 * mag2);
}
async buildVectorIndex() {
console.log('[Vector Search] Starting vector index build');
this.vectorStore.clear();
const files = this.app.vault.getMarkdownFiles();
let processed = 0;
const total = files.length;
for (const file of files) {
console.log(`[Vector Search] Processing file: ${file.path}`);
const content = await this.app.vault.read(file);
const embedding = await this.getEmbedding(content);
this.vectorStore.set(file.path, embedding);
processed++;
if (processed % 10 === 0) {
console.log(`[Vector Search] Progress: ${processed}/${total} files`);
new Notice(`Indexing progress: ${processed}/${total} files`);
}
}
console.log('[Vector Search] Completed vector index build');
}
async searchSimilarNotes(query: string) {
if (this.vectorStore.size === 0) {
new Notice('Vector index is empty. Please rebuild the index first.');
return;
}
const queryEmbedding = await this.getEmbedding(query);
const results: Array<{path: string, similarity: number}> = [];
for (const [path, embedding] of this.vectorStore.entries()) {
const similarity = this.cosineSimilarity(queryEmbedding, embedding);
if (similarity >= this.settings.searchThreshold) {
results.push({ path, similarity });
}
}
results.sort((a, b) => b.similarity - a.similarity);
if (results.length > 0) {
new SimilarNotesModal(this.app, results).open();
} else {
new Notice('No similar notes found');
}
}
}
class SimilarNotesModal extends Modal {
results: Array<{path: string, similarity: number}>;
constructor(app: App, results: Array<{path: string, similarity: number}>) {
super(app);
this.results = results;
}
onOpen() {
const {contentEl} = this;
contentEl.empty();
contentEl.createEl('h2', {text: 'Similar Notes'});
const list = contentEl.createEl('ul');
for (const result of this.results) {
const item = list.createEl('li');
const link = item.createEl('a', {
text: `${result.path} (${(result.similarity * 100).toFixed(2)}%)`,
href: '#'
});
link.addEventListener('click', async (e) => {
e.preventDefault();
const file = this.app.vault.getAbstractFileByPath(result.path);
if (file instanceof TFile) {
await this.app.workspace.activeLeaf.openFile(file);
this.close();
}
});
}
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class VectorSearchSettingTab extends PluginSettingTab {
plugin: VectorSearchPlugin;
constructor(app: App, plugin: VectorSearchPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName('Ollama URL')
.setDesc('URL of your Ollama server')
.addText(text => text
.setPlaceholder('http://localhost:11434')
.setValue(this.plugin.settings.ollamaURL)
.onChange(async (value) => {
this.plugin.settings.ollamaURL = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Search Threshold')
.setDesc('Minimum similarity score (0-1) for showing results')
.addSlider(slider => slider
.setLimits(0, 1, 0.1)
.setValue(this.plugin.settings.searchThreshold)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.searchThreshold = value;
await this.plugin.saveSettings();
}));
}
}

11
manifest.json Normal file
View file

@ -0,0 +1,11 @@
{
"id": "obsidian-vector-search",
"name": "Vector Search",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Semantic search for your notes using Ollama and nomic-embed-text embeddings",
"author": "Your Name",
"authorUrl": "https://github.com/yourusername",
"isDesktopOnly": true,
"dependencies": {}
}

2397
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

8
styles.css Normal file
View file

@ -0,0 +1,8 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/

24
tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"**/*.ts"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}