Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,8 @@ export class SearchResult {
* @returns
*/
export async function *getBestMatches(searchModules: SearchQuotientNode[], timer: ExecutionTimer): AsyncGenerator<SearchResult> {
const spaceQueue = new PriorityQueue<SearchQuotientNode>((a, b) => a.currentCost - b.currentCost);
const comparator = (a: SearchQuotientNode, b: SearchQuotientNode) => a.currentCost - b.currentCost;
let spaceQueue = new PriorityQueue<SearchQuotientNode>(comparator);

// Stage 1 - if we already have extracted results, build a queue just for them
// and iterate over it first.
Expand Down Expand Up @@ -664,6 +665,7 @@ export async function *getBestMatches(searchModules: SearchQuotientNode[], timer
let lowestCostSource = spaceQueue.dequeue();
const newResult = lowestCostSource.handleNextNode();
spaceQueue.enqueue(lowestCostSource);
spaceQueue = new PriorityQueue(comparator, spaceQueue.toArray());

if(newResult.type == 'none') {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
}

this.processed.push(new SearchResult(node));
this.saveResult(node);
return {
type: 'complete',
cost: node.currentCost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export class LegacyQuotientSpur extends SearchQuotientSpur {
const codepointLength = space.codepointLength + insertLength - leftDeleteLength;

super(space, inputs, inputSource, codepointLength);
this.queueNodes(this.buildEdgesForNodes(space.previousResults.map(r => r.node)));
this.insertLength = insertLength;
this.leftDeleteLength = inputSample.deleteLeft;
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { QueueComparator as Comparator, PriorityQueue } from '@keymanapp/web-utils';
import { LexicalModelTypes } from '@keymanapp/common-types';

import { SearchNode, SearchResult } from './distance-modeler.js';
import { SearchNode } from './distance-modeler.js';
import { LegacyQuotientRoot } from './legacy-quotient-root.js';
import { generateSpaceSeed, InputSegment, PathResult, SearchQuotientNode } from './search-quotient-node.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';
Expand All @@ -21,22 +21,23 @@ const PATH_QUEUE_COMPARATOR: Comparator<SearchQuotientNode> = (a, b) => {

// The set of search spaces corresponding to the same 'context' for search.
// Whenever a wordbreak boundary is crossed, a new instance should be made.
export class SearchQuotientCluster implements SearchQuotientNode {
export class SearchQuotientCluster extends SearchQuotientNode {
// While most functions can be done directly from SearchSpace, merging and
// splitting will need access to SearchQuotientSpur-specific members. It's
// also cleaner to not allow nested SearchQuotientClusters while we haven't
// worked out support for such a scenario.
private selectionQueue: PriorityQueue<SearchQuotientNode> = new PriorityQueue(PATH_QUEUE_COMPARATOR);
readonly spaceId: number;

// We use an array and not a PriorityQueue b/c batch-heapifying at a single point in time
// is cheaper than iteratively building a priority queue.
// We use an array and not a PriorityQueue b/c batch-heapifying at a single
// point in time is cheaper than iteratively building a priority queue.
/**
* This tracks all paths that have reached the end of a viable input-matching path - even
* those of lower cost that produce the same correction as other paths.
* This tracks all paths that have reached the end of a viable input-matching
* path - even those of lower cost that produce the same correction as other
* paths.
*
* When new input is received, its entries are then used to append edges to the path in order
* to find potential paths to reach a new viable end.
* When new input is received, its entries are then used to append edges to
* the path in order to find potential paths to reach a new viable end.
*/
private completedPaths?: SearchNode[] = [];

Expand All @@ -63,6 +64,8 @@ export class SearchQuotientCluster implements SearchQuotientNode {
* @param model
*/
constructor(inboundPaths: SearchQuotientNode[]) {
super();

if(inboundPaths.length == 0) {
throw new Error("SearchQuotientCluster requires an array with at least one SearchQuotientNode");
}
Expand Down Expand Up @@ -145,19 +148,17 @@ export class SearchQuotientCluster implements SearchQuotientNode {
const bestPath = this.selectionQueue.dequeue();
const currentResult = bestPath.handleNextNode();
this.selectionQueue.enqueue(bestPath);
this.selectionQueue = new PriorityQueue(PATH_QUEUE_COMPARATOR, this.selectionQueue.toArray());

if(currentResult.type == 'complete') {
this.saveResult(currentResult.finalNode);
this.completedPaths?.push(currentResult.finalNode);
currentResult.spaceId = this.spaceId;
}

return currentResult;
}

public get previousResults(): SearchResult[] {
return this.completedPaths?.map((n => new SearchResult(n, this.spaceId))) ?? [];
}

get model(): LexicalModelTypes.LexicalModel {
return this.parents[0].model;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,44 +96,95 @@ export interface PathInputProperties {
* Represents all or a portion of the dynamically-generated graph used to search
* for predictive-text corrections.
*/
export interface SearchQuotientNode {
export abstract class SearchQuotientNode {
/**
* Holds all `incomingNode` child buffers - buffers to hold nodes processed by
* this SearchCluster but not yet by child SearchSpaces.
*/
private childQueues: SearchNode[][] = [];

/**
* Marks all results that have already been returned from this instance of SearchPath.
* Should be deleted and cleared if any paths consider this one as a parent.
*/
private returnedValues?: {[resultKey: string]: SearchNode} = {};


// The TS type system prevents this method from being rooted on the instance provided in
// the first parameter, sadly.
/**
* Links the provided queueing buffer to the provided parent node. When the
* parent produces new intermediate results, those results will be made
* available for use in construction of extended paths.
* @param parentNode
* @param childQueue
*/
protected linkAndQueueFromParent(parentNode: SearchQuotientNode, childQueue: SearchNode[]): void {
parentNode.childQueues.push(childQueue);
}

/**
* Log the results of a processed node and queue it within all subscribed
* processor nodes for construction of deeper search paths.
* @param node
*/
protected saveResult(node: SearchNode): boolean {
const priorMatch = this.returnedValues[node.resultKey];
if(priorMatch !== undefined && priorMatch.currentCost < node.currentCost) {
return false;
}

this.returnedValues[node.resultKey] = node;
this.childQueues.forEach((buf) => buf.push(node));
return true;
}

/**
* Returns the set of existing, completed search-results with this node's domain.
*/
public get previousResults(): SearchResult[] {
return Object.values(this.returnedValues ?? {}).map(v => new SearchResult(v));
}

// -- Everything after this is abstract and implemented by derived child classes.

/**
* Returns an identifier uniquely identifying this search-batching structure
* by correction-search results.
*/
readonly spaceId: number;
abstract get spaceId(): number;

/**
* The active LexicalModel for use with correction-search.
*/
readonly model: LexicalModel;
abstract get model(): LexicalModel;

/**
* Notes the SearchQuotientNode(s) whose correction-search paths are extended by this
* SearchQuotientNode.
*/
readonly parents: SearchQuotientNode[];
abstract get parents(): SearchQuotientNode[];

/**
* Retrieves the lowest-cost / lowest-distance edge from the batcher's search
* area, checks its validity as a correction to the input text, and reports on
* what sort of result the edge's destination node represents.
* @returns
*/
handleNextNode(): PathResult;
abstract handleNextNode(): PathResult;

/**
* Increases the editing range that will be considered for determining
* correction distances.
*/
increaseMaxEditDistance(): void;
abstract increaseMaxEditDistance(): void;

/**
* Reports the cost of the lowest-cost / lowest-distance edge held within the
* batcher's search area.
* @returns
*/
readonly currentCost: number;
abstract get currentCost(): number;

/**
* Provides a heuristic for the base cost at this path's depth if the best
Expand All @@ -143,19 +194,14 @@ export interface SearchQuotientNode {
* This cost is based on the negative log-likelihood of the probability and
* includes the cost from the lowest possible parent nodes visited.
*/
readonly lowestPossibleSingleCost: number;

/**
* Returns the set of previously-processed results under this batcher's domain.
*/
readonly previousResults: SearchResult[];
abstract readonly lowestPossibleSingleCost: number;

/**
* When true, this indicates that the currently-represented portion of context
* has fat-finger data available, which itself indicates that the user has
* corrections enabled.
*/
readonly correctionsEnabled: boolean;
abstract readonly correctionsEnabled: boolean;

/**
* Reports the total number of input keystrokes represented by this
Expand All @@ -164,32 +210,32 @@ export interface SearchQuotientNode {
* (Their fat-finger alternates, when provided, do not influence this count -
* they're associated with the original keystroke that affected the context.)
*/
readonly inputCount: number;
abstract readonly inputCount: number;

/**
* Reports the length in codepoints of corrected text represented by completed
* paths from this instance.
*/
readonly codepointLength: number;
abstract readonly codepointLength: number;

/**
* Determines the best example text representable by this SearchQuotientNode's
* portion of the correction-search graph and its paths.
*/
readonly bestExample: { text: string, p: number };
abstract readonly bestExample: { text: string, p: number };

/**
* Gets components representing the keystroke range corrected by this
* search-space quotient node. If only part of any keystroke's effects are
* used, this will also be noted.
*/
readonly inputSegments: InputSegment[];
abstract readonly inputSegments: InputSegment[];

/**
* Gets a compact string-based representation of `inputRange` that
* maps compatible token source ranges to each other.
*/
get sourceRangeKey(): string;
abstract get sourceRangeKey(): string;

/**
* Appends this SearchQuotientNode with the provided SearchQuotientNode's search properties,
Expand All @@ -198,7 +244,7 @@ export interface SearchQuotientNode {
* of any split input components will be fully re-merged.
* @param space
*/
merge(space: SearchQuotientNode): SearchQuotientNode;
abstract merge(space: SearchQuotientNode): SearchQuotientNode;

/**
* Splits this SearchQuotientNode into two halves at the specified codepoint index.
Expand All @@ -211,13 +257,13 @@ export interface SearchQuotientNode {
* SearchSpace instance.
* @param charIndex
*/
split(charIndex: number): [SearchQuotientNode, SearchQuotientNode][];
abstract split(charIndex: number): [SearchQuotientNode, SearchQuotientNode][];

/**
* Determines if the SearchQuotientNode is a duplicate of another instance.
* For such cases, the total search space covered by the quotient-graph
* path(s) taken to reach each must be 100% identical.
* @param node
*/
isSameNode(node: SearchQuotientNode): boolean;
abstract isSameNode(node: SearchQuotientNode): boolean;
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@

import { LexicalModelTypes } from '@keymanapp/common-types';

import { SearchNode, SearchResult } from './distance-modeler.js';
import { SearchNode } from './distance-modeler.js';
import { generateSpaceSeed, InputSegment, PathResult, SearchQuotientNode } from './search-quotient-node.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';

import LexicalModel = LexicalModelTypes.LexicalModel;

// The set of search spaces corresponding to the same 'context' for search.
// Whenever a wordbreak boundary is crossed, a new instance should be made.
export class SearchQuotientRoot implements SearchQuotientNode {
export class SearchQuotientRoot extends SearchQuotientNode {
readonly rootNode: SearchNode;
readonly model: LexicalModel;
private readonly rootResult: SearchResult;

readonly lowestPossibleSingleCost: number = 0;

Expand All @@ -23,15 +22,15 @@ export class SearchQuotientRoot implements SearchQuotientNode {
private hasBeenProcessed: boolean = false;

/**
* Constructs a fresh SearchQuotientRoot instance to be used as the root of
* the predictive-text correction / suggestion search process.
* Constructs a fresh SearchSpace instance for used in predictive-text correction
* and suggestion searches.
* @param baseSpaceId
* @param model
*/
constructor(model: LexicalModel) {
super();
this.rootNode = new SearchNode(model.traverseFromRoot(), generateSpaceSeed(), t => model.toKey(t));
this.model = model;
this.rootResult = new SearchResult(this.rootNode);
}

get spaceId(): number {
Expand Down Expand Up @@ -69,6 +68,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {

this.hasBeenProcessed = true;

this.saveResult(this.rootNode);
return {
type: 'complete',
cost: 0,
Expand All @@ -81,14 +81,6 @@ export class SearchQuotientRoot implements SearchQuotientNode {
return this.hasBeenProcessed ? Number.POSITIVE_INFINITY : 0;
}

get previousResults(): SearchResult[] {
if(!this.hasBeenProcessed) {
return [];
} else {
return [this.rootResult];
}
}

// Return a new array each time; avoid aliasing potential!
get inputSegments(): InputSegment[] {
return [];
Expand Down
Loading