#25 - fixed matches algorithm
This commit is contained in:
@@ -9,14 +9,15 @@ import {
|
||||
excerptAfter,
|
||||
type ResultNote,
|
||||
type SearchMatch,
|
||||
} from "../globals"
|
||||
import { loopIndex } from "../utils"
|
||||
} from "src/globals"
|
||||
import { loopIndex } from "src/utils"
|
||||
import { onDestroy, onMount, tick } from "svelte"
|
||||
import { MarkdownView } from "obsidian"
|
||||
import { getSuggestions } from "../search"
|
||||
import { getSuggestions } from "src/search"
|
||||
import ModalContainer from "./ModalContainer.svelte"
|
||||
import type { OmnisearchInFileModal, OmnisearchVaultModal } from "src/modals"
|
||||
import ResultItemInFile from "./ResultItemInFile.svelte"
|
||||
import { Query } from "src/query"
|
||||
|
||||
export let modal: OmnisearchInFileModal
|
||||
export let parent: OmnisearchVaultModal | null = null
|
||||
@@ -26,6 +27,7 @@ export let searchQuery: string
|
||||
let groupedOffsets: number[] = []
|
||||
let selectedIndex = 0
|
||||
let note: ResultNote | null = null
|
||||
let query: Query
|
||||
|
||||
onMount(() => {
|
||||
if (lastSearch && !searchQuery) {
|
||||
@@ -44,7 +46,8 @@ onDestroy(() => {
|
||||
|
||||
$: (async () => {
|
||||
if (searchQuery) {
|
||||
note = (await getSuggestions(searchQuery, { singleFilePath }))[0] ?? null
|
||||
query = new Query(searchQuery)
|
||||
note = (await getSuggestions(query, { singleFilePath }))[0] ?? null
|
||||
lastSearch = searchQuery
|
||||
}
|
||||
selectedIndex = 0
|
||||
|
||||
@@ -7,17 +7,19 @@ import { TFile } from "obsidian"
|
||||
import { onMount, tick } from "svelte"
|
||||
import InputSearch from "./InputSearch.svelte"
|
||||
import ModalContainer from "./ModalContainer.svelte"
|
||||
import { eventBus, type ResultNote } from "../globals"
|
||||
import { createNote, openNote } from "../notes"
|
||||
import { getSuggestions } from "../search"
|
||||
import { loopIndex } from "../utils"
|
||||
import { eventBus, type ResultNote } from "src/globals"
|
||||
import { createNote, openNote } from "src/notes"
|
||||
import { getSuggestions } from "src/search"
|
||||
import { loopIndex } from "src/utils"
|
||||
import { OmnisearchInFileModal, type OmnisearchVaultModal } from "src/modals"
|
||||
import ResultItemVault from "./ResultItemVault.svelte"
|
||||
import { Query } from "src/query"
|
||||
|
||||
export let modal: OmnisearchVaultModal
|
||||
let selectedIndex = 0
|
||||
let searchQuery: string
|
||||
let resultNotes: ResultNote[] = []
|
||||
let query: Query
|
||||
$: selectedNote = resultNotes[selectedIndex]
|
||||
|
||||
$: if (searchQuery) {
|
||||
@@ -37,7 +39,8 @@ onMount(() => {
|
||||
})
|
||||
|
||||
async function updateResults() {
|
||||
resultNotes = await getSuggestions(searchQuery)
|
||||
query = new Query(searchQuery)
|
||||
resultNotes = await getSuggestions(query)
|
||||
lastSearch = searchQuery
|
||||
selectedIndex = 0
|
||||
scrollIntoView()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Query } from "src/query";
|
||||
import type { ResultNote } from "../globals"
|
||||
import { getMatches } from "../search"
|
||||
import { highlighter, makeExcerpt, stringsToRegex } from "../utils"
|
||||
|
||||
18
src/query.ts
18
src/query.ts
@@ -1,4 +1,4 @@
|
||||
import { stripSurroundingQuotes } from './utils'
|
||||
import { escapeRegex, stringsToRegex, stripSurroundingQuotes } from './utils'
|
||||
|
||||
type QueryToken = {
|
||||
/**
|
||||
@@ -16,19 +16,23 @@ type QueryToken = {
|
||||
* This class is used to parse a query string into a structured object
|
||||
*/
|
||||
export class Query {
|
||||
public words: QueryToken[] = []
|
||||
public segments: QueryToken[] = []
|
||||
public exclusions: QueryToken[] = []
|
||||
|
||||
constructor(text: string) {
|
||||
constructor(text = '') {
|
||||
const tokens = parseQuery(text.toLowerCase(), { tokenize: true })
|
||||
this.exclusions = tokens.exclude.text
|
||||
.map(this.formatToken)
|
||||
.filter(o => !!o.value)
|
||||
this.words = tokens.text.map(this.formatToken)
|
||||
this.segments = tokens.text.map(this.formatToken)
|
||||
}
|
||||
|
||||
public getWordsStr(): string {
|
||||
return this.words.map(({ value }) => value).join(' ')
|
||||
public segmentsToStr(): string {
|
||||
return this.segments.map(({ value }) => value).join(' ')
|
||||
}
|
||||
|
||||
public segmentsToRegex(): RegExp {
|
||||
return stringsToRegex(this.segments.map(s => s.value))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +40,7 @@ export class Query {
|
||||
* @returns
|
||||
*/
|
||||
public getExactTerms(): string[] {
|
||||
return this.words.filter(({ exact }) => exact).map(({ value }) => value)
|
||||
return this.segments.filter(({ exact }) => exact).map(({ value }) => value)
|
||||
}
|
||||
|
||||
private formatToken(str: string): QueryToken {
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
type SearchMatch,
|
||||
} from './globals'
|
||||
import {
|
||||
escapeRegex,
|
||||
extractHeadingsFromCache,
|
||||
splitQuotes,
|
||||
stringsToRegex,
|
||||
stripMarkdownCharacters,
|
||||
wait,
|
||||
} from './utils'
|
||||
import { Query } from './query'
|
||||
import type { Query } from './query'
|
||||
|
||||
let minisearchInstance: MiniSearch<IndexedNote>
|
||||
|
||||
let indexedNotes: Record<string, IndexedNote> = {}
|
||||
|
||||
/**
|
||||
@@ -65,8 +65,8 @@ export async function initGlobalSearchIndex(): Promise<void> {
|
||||
* @returns
|
||||
*/
|
||||
async function search(query: Query): Promise<SearchResult[]> {
|
||||
if (!query.getWordsStr()) return []
|
||||
let results = minisearchInstance.search(query.getWordsStr(), {
|
||||
if (!query.segmentsToStr()) return []
|
||||
let results = minisearchInstance.search(query.segmentsToStr(), {
|
||||
prefix: true,
|
||||
fuzzy: term => (term.length > 4 ? 0.2 : false),
|
||||
combineWith: 'AND',
|
||||
@@ -127,11 +127,10 @@ export function getMatches(text: string, reg: RegExp): SearchMatch[] {
|
||||
* @returns
|
||||
*/
|
||||
export async function getSuggestions(
|
||||
queryStr: string,
|
||||
query: Query,
|
||||
options?: Partial<{ singleFilePath: string | null }>,
|
||||
): Promise<ResultNote[]> {
|
||||
// Get the raw results
|
||||
const query = new Query(queryStr)
|
||||
let results = await search(query)
|
||||
if (!results.length) return []
|
||||
|
||||
@@ -153,20 +152,18 @@ export async function getSuggestions(
|
||||
throw new Error(`Note "${result.id}" not indexed`)
|
||||
}
|
||||
|
||||
// Clean search matches that match quoted expresins,
|
||||
// Clean search matches that match quoted expressions,
|
||||
// and inject those expressions instead
|
||||
let words = Object.keys(result.match)
|
||||
const quoted = splitQuotes(query.getWordsStr())
|
||||
for (const quote of quoted) {
|
||||
for (const q of quote.toLowerCase()) {
|
||||
words = words.filter(w => !w.toLowerCase().startsWith(q))
|
||||
}
|
||||
words.push(quote)
|
||||
}
|
||||
const matches = getMatches(note.content, stringsToRegex(words))
|
||||
const foundWords = [
|
||||
...Object.keys(result.match).filter(w =>
|
||||
query.segments.some(s => w.startsWith(s.value)),
|
||||
),
|
||||
...query.segments.filter(s => s.exact).map(s => s.value),
|
||||
]
|
||||
const matches = getMatches(note.content, stringsToRegex(foundWords))
|
||||
const resultNote: ResultNote = {
|
||||
score: result.score,
|
||||
foundWords: words,
|
||||
foundWords,
|
||||
matches,
|
||||
...note,
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export function getAllIndices(text: string, regex: RegExp): SearchMatch[] {
|
||||
}
|
||||
|
||||
export function stringsToRegex(strings: string[]): RegExp {
|
||||
return new RegExp(strings.map(escapeRegex).join('|'), 'gi')
|
||||
return new RegExp(strings.map(s => `(${escapeRegex(s)})`).join('|'), 'gi')
|
||||
}
|
||||
|
||||
export function replaceAll(
|
||||
|
||||
Reference in New Issue
Block a user