Updated the weighting logic + doing some preparation for #6

Headings now have more weight. Also set TS strict mode
This commit is contained in:
Simon Cambier
2022-04-14 20:41:27 +02:00
parent a57582a389
commit 98c277e541
5 changed files with 117 additions and 48 deletions

View File

@@ -1,3 +1,5 @@
import { match } from 'assert'
// Matches a wikiling that begins a string // Matches a wikiling that begins a string
export const regexWikilink = /^!?\[\[(?<name>.+?)(\|(?<alias>.+?))?\]\]/ export const regexWikilink = /^!?\[\[(?<name>.+?)(\|(?<alias>.+?))?\]\]/
export const regexLineSplit = /\r?\n|\r|((\.|\?|!)( |\r?\n|\r))/g export const regexLineSplit = /\r?\n|\r|((\.|\?|!)( |\r?\n|\r))/g
@@ -13,12 +15,23 @@ export type IndexedNote = {
path: string path: string
basename: string basename: string
content: string content: string
headings1: string
headings2: string
headings3: string
}
export type SearchMatch = {
match: string
index: number
}
export const isSearchMatch = (o: { index?: number }): o is SearchMatch => {
return o.index !== undefined
} }
export type ResultNote = { export type ResultNote = {
path: string path: string
basename: string basename: string
content: string content: string
keyword: string matches: SearchMatch[]
occurence: number occurence: number
} }

View File

@@ -1,13 +1,17 @@
import { Notice, Plugin, TAbstractFile, TFile } from 'obsidian' import { Notice, Plugin, TAbstractFile, TFile } from 'obsidian'
import MiniSearch from 'minisearch' import MiniSearch from 'minisearch'
import { clearContent, getTitleLine, wait } from './utils' import {
clearContent,
extractHeadingsFromCache,
wait,
} from './utils'
import { IndexedNote } from './globals' import { IndexedNote } from './globals'
import { OmnisearchModal } from './modal' import { OmnisearchModal } from './modal'
export default class OmnisearchPlugin extends Plugin { export default class OmnisearchPlugin extends Plugin {
minisearch: MiniSearch<IndexedNote> minisearch!: MiniSearch<IndexedNote>
lastSearch?: string lastSearch?: string
indexedNotes: Record<string, IndexedNote> indexedNotes: Record<string, IndexedNote> = {}
async onload(): Promise<void> { async onload(): Promise<void> {
await this.instantiateMinisearch() await this.instantiateMinisearch()
@@ -53,11 +57,7 @@ export default class OmnisearchPlugin extends Plugin {
this.indexedNotes = {} this.indexedNotes = {}
this.minisearch = new MiniSearch({ this.minisearch = new MiniSearch({
idField: 'path', idField: 'path',
fields: ['content', 'title', 'basename'], fields: ['basename', 'content', 'headings1', 'headings2', 'headings3'],
extractField: (document, fieldname: 'content' | 'title' | 'basename') => {
if (fieldname === 'title') return getTitleLine(document.content)
return document[fieldname]
},
}) })
// Index files that are already present // Index files that are already present
@@ -89,6 +89,8 @@ export default class OmnisearchPlugin extends Plugin {
if (!(file instanceof TFile) || file.extension !== 'md') return if (!(file instanceof TFile) || file.extension !== 'md') return
try { try {
// console.log(`Omnisearch - adding ${file.path} to index`) // console.log(`Omnisearch - adding ${file.path} to index`)
const fileCache = this.app.metadataCache.getFileCache(file)
// console.log(fileCache)
if (this.indexedNotes[file.path]) { if (this.indexedNotes[file.path]) {
throw new Error(`${file.basename} is already indexed`) throw new Error(`${file.basename} is already indexed`)
@@ -106,6 +108,9 @@ export default class OmnisearchPlugin extends Plugin {
basename: file.basename, basename: file.basename,
content: tmp.innerText, content: tmp.innerText,
path: file.path, path: file.path,
headings1: fileCache ? extractHeadingsFromCache(fileCache, 1).join(' ') : '',
headings2: fileCache ? extractHeadingsFromCache(fileCache, 2).join(' ') : '',
headings3: fileCache ? extractHeadingsFromCache(fileCache, 3).join(' ') : '',
} }
this.minisearch.add(note) this.minisearch.add(note)
this.indexedNotes[file.path] = note this.indexedNotes[file.path] = note

View File

@@ -1,7 +1,7 @@
import { MarkdownView, SuggestModal, TFile } from 'obsidian' import { MarkdownView, SuggestModal, TFile } from 'obsidian'
import { ResultNote } from './globals' import { ResultNote } from './globals'
import OmnisearchPlugin from './main' import OmnisearchPlugin from './main'
import { escapeRegex, highlighter } from './utils' import { escapeRegex, getAllIndexes, highlighter } from './utils'
export class OmnisearchModal extends SuggestModal<ResultNote> { export class OmnisearchModal extends SuggestModal<ResultNote> {
private plugin: OmnisearchPlugin private plugin: OmnisearchPlugin
@@ -43,9 +43,13 @@ export class OmnisearchModal extends SuggestModal<ResultNote> {
await this.app.workspace.openLinkText(file.path, '') await this.app.workspace.openLinkText(file.path, '')
} }
catch (e) { catch (e) {
if (e.message === 'File already exists.') { if (e instanceof Error && e.message === 'File already exists.') {
// Open the existing file instead of creating it
await this.app.workspace.openLinkText(this.inputEl.value, '') await this.app.workspace.openLinkText(this.inputEl.value, '')
} }
else {
console.error(e)
}
} }
} }
this.close() this.close()
@@ -60,7 +64,7 @@ export class OmnisearchModal extends SuggestModal<ResultNote> {
const record = events.find(event => const record = events.find(event =>
(event.target as HTMLDivElement).classList.contains('is-selected'), (event.target as HTMLDivElement).classList.contains('is-selected'),
) )
const id = (record?.target as HTMLElement).getAttribute('data-note-id') const id = (record?.target as HTMLElement)?.getAttribute('data-note-id') ?? null
if (id) { if (id) {
this.selectedNoteId = id this.selectedNoteId = id
} }
@@ -91,7 +95,9 @@ export class OmnisearchModal extends SuggestModal<ResultNote> {
} }
onClose(): void { onClose(): void {
this.mutationObserver.disconnect() if (this.mutationObserver) {
this.mutationObserver.disconnect()
}
} }
async getSuggestions(query: string): Promise<ResultNote[]> { async getSuggestions(query: string): Promise<ResultNote[]> {
@@ -102,47 +108,55 @@ export class OmnisearchModal extends SuggestModal<ResultNote> {
prefix: true, prefix: true,
fuzzy: term => (term.length > 4 ? 0.2 : false), fuzzy: term => (term.length > 4 ? 0.2 : false),
combineWith: 'AND', combineWith: 'AND',
boost: { basename: 2, title: 1.5 }, boost: { basename: 2, headings1: 1.5, headings2: 1.3, headings3: 1.1 },
}) })
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
.slice(0, 50) .slice(0, 50)
// console.log('Omnisearch - Results:') // console.log(`Omnisearch - Results for "${query}"`)
// console.log(results) // console.log(results)
return results.map(result => { const suggestions = await Promise.all(
const note = this.plugin.indexedNotes[result.id] results.map(async result => {
let content = note.content const file = this.app.vault.getAbstractFileByPath(result.id) as TFile
let basename = note.basename // const metadata = this.app.metadataCache.getFileCache(file)
let content = (await this.app.vault.cachedRead(file)).toLowerCase()
let basename = file.basename
// If the body contains a searched term, find its position // Sort the terms from smaller to larger
// and trim the text around it // and highlight them in the title and body
const pos = content.toLowerCase().indexOf(result.terms[0]) const terms = result.terms.sort((a, b) => a.length - b.length)
const surroundLen = 180 const reg = new RegExp(terms.map(escapeRegex).join('|'), 'gi')
if (pos > -1) { const matches = getAllIndexes(content, reg)
const from = Math.max(0, pos - surroundLen)
const to = Math.min(content.length - 1, pos + surroundLen)
content =
(from > 0 ? '…' : '') +
content.slice(from, to).trim() +
(to < content.length - 1 ? '…' : '')
}
// Sort the terms from smaller to larger // If the body contains a searched term, find its position
// and highlight them in the title and body // and trim the text around it
const terms = result.terms.sort((a, b) => a.length - b.length) const pos = content.toLowerCase().indexOf(result.terms[0])
const reg = new RegExp(terms.map(escapeRegex).join('|'), 'gi') const surroundLen = 180
content = content.replace(reg, highlighter) if (pos > -1) {
basename = basename.replace(reg, highlighter) const from = Math.max(0, pos - surroundLen)
const to = Math.min(content.length - 1, pos + surroundLen)
content =
(from > 0 ? '…' : '') +
content.slice(from, to).trim() +
(to < content.length - 1 ? '…' : '')
}
const resultNote: ResultNote = { // console.log(matches)
content, content = content.replace(reg, highlighter)
basename, basename = basename.replace(reg, highlighter)
path: note.path,
keyword: result.terms[0], const resultNote: ResultNote = {
occurence: 0, content,
} basename,
return resultNote path: file.path,
}) matches,
occurence: 0,
}
return resultNote
}),
)
return suggestions
} }
renderSuggestion(value: ResultNote, el: HTMLElement): void { renderSuggestion(value: ResultNote, el: HTMLElement): void {
@@ -160,11 +174,18 @@ export class OmnisearchModal extends SuggestModal<ResultNote> {
async onChooseSuggestion(item: ResultNote): Promise<void> { async onChooseSuggestion(item: ResultNote): Promise<void> {
const file = this.app.vault.getAbstractFileByPath(item.path) as TFile const file = this.app.vault.getAbstractFileByPath(item.path) as TFile
// const fileCache = this.app.metadataCache.getFileCache(file)
// console.log(fileCache)
const content = (await this.app.vault.cachedRead(file)).toLowerCase() const content = (await this.app.vault.cachedRead(file)).toLowerCase()
const offset = content.indexOf(item.keyword.toLowerCase()) const offset = content.indexOf(
item.matches[item.occurence].match.toLowerCase(),
)
await this.app.workspace.openLinkText(item.path, '') await this.app.workspace.openLinkText(item.path, '')
const view = this.app.workspace.getActiveViewOfType(MarkdownView) const view = this.app.workspace.getActiveViewOfType(MarkdownView)
if (!view) {
throw new Error('OmniSearch - No active MarkdownView')
}
const pos = view.editor.offsetToPos(offset) const pos = view.editor.offsetToPos(offset)
pos.ch = 0 pos.ch = 0

View File

@@ -1,5 +1,12 @@
import markdownToTxt from 'markdown-to-txt' import markdownToTxt from 'markdown-to-txt'
import { regexLineSplit, regexWikilink, regexYaml } from './globals' import { CachedMetadata } from 'obsidian'
import {
isSearchMatch,
regexLineSplit,
regexWikilink,
regexYaml,
SearchMatch,
} from './globals'
export function highlighter(str: string): string { export function highlighter(str: string): string {
return '<span class="search-result-file-matched-text">' + str + '</span>' return '<span class="search-result-file-matched-text">' + str + '</span>'
@@ -64,3 +71,25 @@ export function wait(ms: number): Promise<void> {
export function escapeRegex(str: string): string { export function escapeRegex(str: string): string {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
} }
/**
* Returns the positions of all occurences of `val` inside of `text`
* https://stackoverflow.com/a/58828841
* @param text
* @param val
* @returns
*/
export function getAllIndexes(text: string, val: RegExp): SearchMatch[] {
return [...text.matchAll(val)]
.map(o => ({ match: o[0], index: o.index }))
.filter(isSearchMatch)
}
export function extractHeadingsFromCache(
cache: CachedMetadata,
level: number,
): string[] {
return (
cache.headings?.filter(h => h.level === level).map(h => h.heading) ?? []
)
}

View File

@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true,
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true, "inlineSourceMap": true,
"inlineSources": true, "inlineSources": true,