#25 - fixed matches algorithm

This commit is contained in:
Simon Cambier
2022-04-30 20:18:26 +02:00
parent f2938cde88
commit ca04ed80c9
6 changed files with 42 additions and 34 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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,
} }

View File

@@ -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(