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