Make query optimizer pluggable

This commit is contained in:
2023-09-11 15:33:16 +01:00
parent a4c1ba8450
commit e2e1bb90ca
16 changed files with 164 additions and 65 deletions

View File

@ -1,7 +1,7 @@
import { ReqFilter, UsersRelays } from ".";
import { dedupe, unwrap } from "@snort/shared";
import debug from "debug";
import { FlatReqFilter } from "request-expander";
import { FlatReqFilter } from "./query-optimizer";
const PickNRelays = 2;

View File

@ -1,9 +1,11 @@
import { AuthHandler, RelaySettings, ConnectionStateSnapshot } from "./connection";
import { RequestBuilder } from "./request-builder";
import { NoteStore, NoteStoreHook, NoteStoreSnapshotData } from "./note-collection";
import { NoteStore, NoteStoreSnapshotData } from "./note-collection";
import { Query } from "./query";
import { NostrEvent, ReqFilter, TaggedNostrEvent } from "./nostr";
import { ProfileLoaderService } from "./profile-cache";
import { RelayCache } from "./gossip-model";
import { QueryOptimizer } from "./query-optimizer";
export * from "./nostr-system";
export { default as EventKind } from "./event-kind";
@ -24,6 +26,7 @@ export * from "./signer";
export * from "./text";
export * from "./pow";
export * from "./pow-util";
export * from "./query-optimizer";
export * from "./impl/nip4";
export * from "./impl/nip44";
@ -96,6 +99,16 @@ export interface SystemInterface {
* Profile cache/loader
*/
get ProfileLoader(): ProfileLoaderService;
/**
* Relay cache for "Gossip" model
*/
get RelayCache(): RelayCache;
/**
* Query optimizer
*/
get QueryOptimizer(): QueryOptimizer;
}
export interface SystemSnapshot {
@ -121,4 +134,4 @@ export interface MessageEncryptor {
getSharedSecret(privateKey: string, publicKey: string): Promise<Uint8Array> | Uint8Array;
encryptData(plaintext: string, sharedSecet: Uint8Array): Promise<MessageEncryptorPayload> | MessageEncryptorPayload;
decryptData(payload: MessageEncryptorPayload, sharedSecet: Uint8Array): Promise<string> | string;
}
}

View File

@ -20,6 +20,8 @@ import {
UsersRelays,
} from ".";
import { EventsCache } from "./cache/events";
import { RelayCache } from "./gossip-model";
import { QueryOptimizer, DefaultQueryOptimizer } from "./query-optimizer";
/**
* Manages nostr content retrieval system
@ -72,12 +74,18 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
*/
#eventsCache: FeedCache<NostrEvent>;
/**
* Query optimizer instance
*/
#queryOptimizer: QueryOptimizer;
constructor(props: {
authHandler?: AuthHandler;
relayCache?: FeedCache<UsersRelays>;
profileCache?: FeedCache<MetadataCache>;
relayMetrics?: FeedCache<RelayMetrics>;
eventsCache?: FeedCache<NostrEvent>;
queryOptimizer?: QueryOptimizer;
}) {
super();
this.#handleAuth = props.authHandler;
@ -85,6 +93,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
this.#profileCache = props.profileCache ?? new UserProfileCache();
this.#relayMetricsCache = props.relayMetrics ?? new RelayMetricCache();
this.#eventsCache = props.eventsCache ?? new EventsCache();
this.#queryOptimizer = props.queryOptimizer ?? DefaultQueryOptimizer;
this.#profileLoader = new ProfileLoaderService(this, this.#profileCache);
this.#relayMetrics = new RelayMetricHandler(this.#relayMetricsCache);
@ -92,9 +101,6 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
}
HandleAuth?: AuthHandler | undefined;
/**
* Profile loader service allows you to request profiles
*/
get ProfileLoader() {
return this.#profileLoader;
}
@ -103,6 +109,14 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
return [...this.#sockets.values()].map(a => a.snapshot());
}
get RelayCache(): RelayCache {
return this.#relayCache;
}
get QueryOptimizer(): QueryOptimizer {
return this.#queryOptimizer;
}
/**
* Setup caches
*/
@ -241,8 +255,8 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
return existing;
}
const filters = !req.options?.skipDiff
? req.buildDiff(this.#relayCache, existing.filters)
: req.build(this.#relayCache);
? req.buildDiff(this, existing.filters)
: req.build(this);
if (filters.length === 0 && !!req.options?.skipDiff) {
return existing;
} else {
@ -255,7 +269,7 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
} else {
const store = new type();
const filters = req.build(this.#relayCache);
const filters = req.build(this);
const q = new Query(req.id, req.instance, store, req.options?.leaveOpen);
if (filters.some(a => a.filters.some(b => b.ids))) {
const expectIds = new Set(filters.flatMap(a => a.filters).flatMap(a => a.ids ?? []));
@ -397,4 +411,4 @@ export class NostrSystem extends ExternalStore<SystemSnapshot> implements System
}
setTimeout(() => this.#cleanup(), 1_000);
}
}
}

View File

@ -0,0 +1,43 @@
import { ReqFilter } from "../nostr"
import { expandFilter } from "./request-expander"
import { flatMerge, mergeSimilar } from "./request-merger"
import { diffFilters } from "./request-splitter"
export interface FlatReqFilter {
keys: number;
ids?: string;
authors?: string;
kinds?: number;
"#e"?: string;
"#p"?: string;
"#t"?: string;
"#d"?: string;
"#r"?: string;
search?: string;
since?: number;
until?: number;
limit?: number;
}
export interface QueryOptimizer {
expandFilter(f: ReqFilter): Array<FlatReqFilter>
getDiff(prev: Array<ReqFilter>, next: Array<ReqFilter>): Array<FlatReqFilter>
flatMerge(all: Array<FlatReqFilter>): Array<ReqFilter>
compress(all: Array<ReqFilter>): Array<ReqFilter>
}
export const DefaultQueryOptimizer = {
expandFilter: (f: ReqFilter) => {
return expandFilter(f);
},
getDiff: (prev: Array<ReqFilter>, next: Array<ReqFilter>) => {
const diff = diffFilters(prev.flatMap(a => expandFilter(a)), next.flatMap(a => expandFilter(a)));
return diff.added;
},
flatMerge: (all: Array<FlatReqFilter>) => {
return flatMerge(all);
},
compress: (all: Array<ReqFilter>) => {
return mergeSimilar(all);
}
} as QueryOptimizer;

View File

@ -1,27 +1,11 @@
import { ReqFilter } from "./nostr";
import {expand_filter} from "@snort/system-query";
export interface FlatReqFilter {
keys: number;
ids?: string;
authors?: string;
kinds?: number;
"#e"?: string;
"#p"?: string;
"#t"?: string;
"#d"?: string;
"#r"?: string;
search?: string;
since?: number;
until?: number;
limit?: number;
}
import { FlatReqFilter } from ".";
import { ReqFilter } from "../nostr";
/**
* Expand a filter into its most fine grained form
*/
export function expandFilter(f: ReqFilter): Array<FlatReqFilter> {
/*const ret: Array<FlatReqFilter> = [];
const ret: Array<FlatReqFilter> = [];
const src = Object.entries(f);
const keys = src.filter(([, v]) => Array.isArray(v)).map(a => a[0]);
const props = src.filter(([, v]) => !Array.isArray(v));
@ -47,8 +31,5 @@ export function expandFilter(f: ReqFilter): Array<FlatReqFilter> {
...Object.fromEntries(props),
});
return ret;*/
const ret = expand_filter(f);
return ret as Array<FlatReqFilter>;
return ret;
}

View File

@ -1,6 +1,6 @@
import { distance } from "@snort/shared";
import { ReqFilter } from ".";
import { FlatReqFilter } from "./request-expander";
import { ReqFilter } from "..";
import { FlatReqFilter } from ".";
/**
* Keys which can change the entire meaning of the filter outside the array types

View File

@ -1,9 +1,8 @@
import { flatFilterEq } from "./utils";
import { FlatReqFilter } from "./request-expander";
import { diff_filters } from "@snort/system-query";
import { flatFilterEq } from "../utils";
import { FlatReqFilter } from ".";
export function diffFilters(prev: Array<FlatReqFilter>, next: Array<FlatReqFilter>, calcRemoved?: boolean) {
/*const added = [];
const added = [];
const removed = [];
for (const n of next) {
@ -29,12 +28,5 @@ export function diffFilters(prev: Array<FlatReqFilter>, next: Array<FlatReqFilte
added: changed ? added : [],
removed: changed ? removed : [],
changed,
};*/
const added = diff_filters(prev, next);
return {
changed: added.length > 0,
added: (added as Array<FlatReqFilter>),
removed: []
}
};
}

View File

@ -1,12 +1,11 @@
import debug from "debug";
import { v4 as uuid } from "uuid";
import { appendDedupe, sanitizeRelayUrl, unixNowMs } from "@snort/shared";
import { flat_merge, get_diff }from "@snort/system-query";
import { ReqFilter, u256, HexKey, EventKind } from ".";
import EventKind from "./event-kind";
import { SystemInterface } from "index";
import { ReqFilter, u256, HexKey } from "./nostr";
import { RelayCache, splitByWriteRelays, splitFlatByWriteRelays } from "./gossip-model";
import { flatMerge, mergeSimilar } from "./request-merger";
import { FlatReqFilter } from "./request-expander";
/**
* Which strategy is used when building REQ filters
@ -95,27 +94,25 @@ export class RequestBuilder {
return this.#builders.map(f => f.filter);
}
build(relays: RelayCache): Array<BuiltRawReqFilter> {
const expanded = this.#builders.flatMap(a => a.build(relays, this.id));
return this.#groupByRelay(expanded);
build(system: SystemInterface): Array<BuiltRawReqFilter> {
const expanded = this.#builders.flatMap(a => a.build(system.RelayCache, this.id));
return this.#groupByRelay(system, expanded);
}
/**
* Detects a change in request from a previous set of filters
*/
buildDiff(relays: RelayCache, prev: Array<ReqFilter>): Array<BuiltRawReqFilter> {
buildDiff(system: SystemInterface, prev: Array<ReqFilter>): Array<BuiltRawReqFilter> {
const start = unixNowMs();
//const next = this.#builders.flatMap(f => expandFilter(f.filter));
//const diff = diffFilters(prev, next);
const diff = get_diff(prev, this.buildRaw()) as Array<FlatReqFilter>;
const diff = system.QueryOptimizer.getDiff(prev, this.buildRaw());
const ts = unixNowMs() - start;
this.#log("buildDiff %s %d ms", this.id, ts);
if (diff.length > 0) {
return splitFlatByWriteRelays(relays, diff).map(a => {
return splitFlatByWriteRelays(system.RelayCache, diff).map(a => {
return {
strategy: RequestStrategy.AuthorsRelays,
filters: flat_merge(a.filters) as Array<ReqFilter>,
filters: system.QueryOptimizer.flatMerge(a.filters),
relay: a.relay,
};
});
@ -130,7 +127,7 @@ export class RequestBuilder {
* @param expanded
* @returns
*/
#groupByRelay(expanded: Array<BuiltRawReqFilter>) {
#groupByRelay(system: SystemInterface, expanded: Array<BuiltRawReqFilter>) {
const relayMerged = expanded.reduce((acc, v) => {
const existing = acc.get(v.relay);
if (existing) {
@ -143,7 +140,7 @@ export class RequestBuilder {
const filtersSquashed = [...relayMerged.values()].map(a => {
return {
filters: mergeSimilar(a.flatMap(b => b.filters)),
filters: system.QueryOptimizer.compress(a.flatMap(b => b.filters)),
relay: a[0].relay,
strategy: a[0].strategy,
} as BuiltRawReqFilter;

View File

@ -6,6 +6,8 @@ import { NostrEvent, TaggedNostrEvent } from "./nostr";
import { NoteStore, NoteStoreSnapshotData } from "./note-collection";
import { Query } from "./query";
import { RequestBuilder } from "./request-builder";
import { RelayCache } from "./gossip-model";
import { QueryOptimizer } from "./query-optimizer";
export class SystemWorker extends ExternalStore<SystemSnapshot> implements SystemInterface {
#port: MessagePort;
@ -29,6 +31,13 @@ export class SystemWorker extends ExternalStore<SystemSnapshot> implements Syste
throw new Error("Method not implemented.");
}
get RelayCache(): RelayCache {
throw new Error("Method not implemented.");
}
get QueryOptimizer(): QueryOptimizer {
throw new Error("Method not implemented.");
}
HandleAuth?: AuthHandler;
get Sockets(): ConnectionStateSnapshot[] {

View File

@ -1,5 +1,5 @@
import { equalProp } from "@snort/shared";
import { FlatReqFilter } from "./request-expander";
import { FlatReqFilter } from "./query-optimizer";
import { NostrEvent, ReqFilter } from "./nostr";
export function findTag(e: NostrEvent, tag: string) {