1
0
silverbullet/plugs/search/search.ts

147 lines
3.9 KiB
TypeScript
Raw Normal View History

import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
2022-10-14 13:11:33 +00:00
import { renderToText } from "$sb/lib/tree.ts";
import { applyQuery } from "$sb/lib/query.ts";
2023-08-28 15:12:15 +00:00
import { editor, index, store } from "$sb/syscalls.ts";
import { BatchKVStore, SimpleSearchEngine } from "./engine.ts";
2023-08-20 15:51:00 +00:00
import { FileMeta } from "$sb/types.ts";
2023-08-29 19:17:29 +00:00
import { PromiseQueue } from "$sb/lib/async.ts";
2022-05-16 13:09:36 +00:00
const searchPrefix = "🔍 ";
2023-08-30 19:12:33 +00:00
class StoreKVStore implements BatchKVStore {
constructor(private prefix: string) {
}
2023-08-30 20:16:14 +00:00
async queryPrefix(prefix: string): Promise<[string, any][]> {
const results = await store.queryPrefix(this.prefix + prefix);
return results.map((
{ key, value },
) => [key.substring(this.prefix.length), value]);
}
get(keys: string[]): Promise<(string[] | undefined)[]> {
return store.batchGet(keys.map((key) => this.prefix + key));
}
set(entries: Map<string, string[]>): Promise<void> {
return store.batchSet(
Array.from(entries.entries()).map((
[key, value],
) => ({ key: this.prefix + key, value })),
);
}
delete(keys: string[]): Promise<void> {
return store.batchDel(keys.map((key) => this.prefix + key));
}
}
2023-08-28 15:12:15 +00:00
const ftsKvStore = new StoreKVStore("fts:");
const ftsRevKvStore = new StoreKVStore("fts_rev:");
const engine = new SimpleSearchEngine(ftsKvStore, ftsRevKvStore);
2023-08-29 19:17:29 +00:00
// Search indexing is prone to concurrency issues, so we queue all write operations
const promiseQueue = new PromiseQueue();
export function indexPage({ name, tree }: IndexTreeEvent) {
const text = renderToText(tree);
2023-08-29 19:17:29 +00:00
return promiseQueue.runInQueue(async () => {
// console.log("Now FTS indexing", name);
await engine.deleteDocument(name);
await engine.indexDocument({ id: name, text });
});
}
export async function clearIndex() {
await store.deletePrefix("fts:");
await store.deletePrefix("fts_rev:");
2022-05-16 13:09:36 +00:00
}
2023-08-29 19:17:29 +00:00
export function pageUnindex(pageName: string) {
return promiseQueue.runInQueue(() => {
return engine.deleteDocument(pageName);
});
2022-06-28 12:14:15 +00:00
}
2022-05-16 13:09:36 +00:00
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<any[]> {
2022-10-14 13:11:33 +00:00
const phraseFilter = query.filter.find((f) => f.prop === "phrase");
2022-05-16 13:09:36 +00:00
if (!phraseFilter) {
throw Error("No 'phrase' filter specified, this is mandatory");
}
let results: any[] = await engine.search(phraseFilter.value);
// Patch the object to a format that users expect (translate id to name)
for (const r of results) {
r.name = r.id;
delete r.id;
}
2022-05-16 13:09:36 +00:00
2022-10-14 13:11:33 +00:00
const allPageMap: Map<string, any> = new Map(
2022-10-12 09:47:13 +00:00
results.map((r: any) => [r.name, r]),
2022-05-17 06:32:33 +00:00
);
2022-10-14 13:11:33 +00:00
for (const { page, value } of await index.queryPrefix("meta:")) {
const p = allPageMap.get(page);
2022-05-16 13:09:36 +00:00
if (p) {
2022-10-15 17:02:56 +00:00
for (const [k, v] of Object.entries(value)) {
2022-05-16 13:09:36 +00:00
p[k] = v;
}
}
}
// Remove the "phrase" filter
query.filter.splice(query.filter.indexOf(phraseFilter), 1);
results = applyQuery(query, results);
return results;
}
2022-05-17 09:53:17 +00:00
export async function searchCommand() {
2022-10-19 07:52:29 +00:00
const phrase = await editor.prompt("Search for: ");
2022-05-17 09:53:17 +00:00
if (phrase) {
2022-10-14 13:11:33 +00:00
await editor.navigate(`${searchPrefix}${phrase}`);
2022-05-17 09:53:17 +00:00
}
}
2022-10-19 07:52:29 +00:00
export async function readFileSearch(
2022-10-12 09:47:13 +00:00
name: string,
): Promise<{ data: Uint8Array; meta: FileMeta }> {
2022-10-19 07:52:29 +00:00
const phrase = name.substring(
searchPrefix.length,
name.length - ".md".length,
);
const results = await engine.search(phrase);
2022-10-12 09:47:13 +00:00
const text = `# Search results for "${phrase}"\n${
results
.map((r) => `* [[${r.id}]] (score ${r.score})`)
.join("\n")
2022-10-12 09:47:13 +00:00
}
`;
2022-10-19 07:52:29 +00:00
2022-05-17 09:53:17 +00:00
return {
data: new TextEncoder().encode(text),
2022-05-17 09:53:17 +00:00
meta: {
name,
2022-10-19 07:52:29 +00:00
contentType: "text/markdown",
size: text.length,
2022-05-17 09:53:17 +00:00
lastModified: 0,
perm: "ro",
},
};
}
export function writeFileSearch(
name: string,
): FileMeta {
// Never actually writing this
return getFileMetaSearch(name);
}
2022-10-19 07:52:29 +00:00
export function getFileMetaSearch(name: string): FileMeta {
2022-05-17 09:53:17 +00:00
return {
name,
2022-10-19 07:52:29 +00:00
contentType: "text/markdown",
size: -1,
2022-05-17 09:53:17 +00:00
lastModified: 0,
perm: "ro",
};
}