Skip to content
Open
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
@@ -0,0 +1,36 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by jahorton on 2026-04-02
*
* This file defines the type used for tracking critical graph-search properties
* utilized during correction-search by any type compatible with the
* `getBestMatches` algorithm.
*/

import { CorrectionSearchable } from "./correction-searchable.js";

/**
* Any return value from `.handleNextNode()` designed for use with the
* `getBestMatches` method must adhere to this type interface for representing
* completed search paths.
*/
export interface CorrectionResultMapping<ResultType> {
/**
* Represents the "searchable" object through which the completed search path last traversed.
*/
readonly matchingSpace: CorrectionSearchable<ResultType, CorrectionResultMapping<ResultType>>;

/**
* The object representing the search path completed at the current search step.
*/
readonly matchedResult: ResultType;

/**
* Gets the "total cost" of the edge, which should be considered as the
* negative log-likelihood of the input path taken to reach the node
* multiplied by the 'probability' induced by needed Damerau-Levenshtein edits
* to the resulting output.
*/
readonly totalCost: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by jahorton on 2026-04-02
*
* This file defines the interface required of objects that represent portions
* of a search graph or search quotient-graph compatible with the
* `getBestMatches` algorithm.
*/

import { CorrectionResultMapping } from "./correction-result-mapping.js";

type NullPath = {
type: 'none'
}

type IntermediateSearchPath = {
type: 'intermediate',
cost: number
}

type CompleteSearchPath<MappingType> = {
type: 'complete',
cost: number,
mapping: MappingType,
spaceId: number
}

export type PathResult<MappingType> = NullPath | IntermediateSearchPath | CompleteSearchPath<MappingType>;

/**
* Represents objects that support correction search via the `getBestMatches`
* method, providing metadata relative to optimizing the search process for
* their represented portion of the search-graph.
*/
export interface CorrectionSearchable<ResultType, ResultMapping extends CorrectionResultMapping<ResultType>> {
/**
* The best cost found for any search paths yet unprocessed by either this "searchable" or any of its ancestors.
*/
readonly currentCost: number;

/**
* A list of all search path results already found that terminate at this "searchable".
*/
readonly previousResults: ResultMapping[];

/**
* Processes the most likely currently-unprocessed search path represented by this "searchable".
*/
handleNextNode(): PathResult<ResultMapping>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { PriorityQueue } from '@keymanapp/web-utils';
import { LexicalModelTypes } from '@keymanapp/common-types';

import { ClassicalDistanceCalculation } from './classical-calculation.js';
import { CorrectionSearchable } from './correction-searchable.js';
import { CorrectionResultMapping } from './correction-result-mapping.js';
import { ExecutionTimer, STANDARD_TIME_BETWEEN_DEFERS } from './execution-timer.js';
import { SearchQuotientNode } from './search-quotient-node.js';
import { initTokenResultFilterer, TokenResultMapping } from './token-result-mapping.js';
Expand Down Expand Up @@ -581,7 +583,7 @@ export class SearchNode {
* @returns
*/
export const getBestTokenMatches = (searchModules: SearchQuotientNode[], timer?: ExecutionTimer) => {
return getBestMatches(searchModules, timer, initTokenResultFilterer());
return getBestMatches<SearchNode, TokenResultMapping, SearchQuotientNode>(searchModules, timer, initTokenResultFilterer());
}

/**
Expand All @@ -592,30 +594,38 @@ export const getBestTokenMatches = (searchModules: SearchQuotientNode[], timer?:
* @param timer
* @returns
*/
export async function *getBestMatches(
searchModules: SearchQuotientNode[],
export async function *getBestMatches<
// metadata / analysis of search path results - gives the corrections
ResultType,
// associates analysis with its generating search-space, provides interface needed for correction-search evaluations
ResultMapping extends CorrectionResultMapping<ResultType>,
// the type managing the search - SearchQuotientNode (for tokens) or TokenizationCorrector (for tokenizations)
Correctable extends CorrectionSearchable<ResultType, ResultMapping>
> (
searchModules: Correctable[],
timer: ExecutionTimer,
filter?: (searchResult: TokenResultMapping) => boolean
): AsyncGenerator<TokenResultMapping> {
filter?: (searchResult: ResultMapping) => boolean
): AsyncGenerator<ResultMapping> {
// If no filter function is provided, default to one that always returns true.
filter ??= () => true;

const spaceQueue = new PriorityQueue<SearchQuotientNode>((a, b) => a.currentCost - b.currentCost);
const spaceQueue = new PriorityQueue<Correctable>((a, b) => a.currentCost - b.currentCost);

// Stage 1 - if we already have extracted results, build a queue just for them
// and iterate over it first.
//
// Does not get any results that another iterator pulls up after this is
// created - and those results won't come up later in stage 2, either. Only
// intended for restarting a search, not searching twice in parallel.
const priorResultsQueue = new PriorityQueue<TokenResultMapping>((a, b) => a.totalCost - b.totalCost);
const priorResultsQueue = new PriorityQueue<ResultMapping>((a, b) => a.totalCost - b.totalCost);
priorResultsQueue.enqueueAll(searchModules.map((space) => space.previousResults).flat());

// With potential prior results re-queued, NOW enqueue. (Not before - the heap may reheapify!)
spaceQueue.enqueueAll(searchModules);

// Stage 2: the fun part; actually searching!
do {
const entry: TokenResultMapping = timer.time(() => {
const entry: ResultMapping = timer.time(() => {
if((priorResultsQueue.peek()?.totalCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) {
const result = priorResultsQueue.dequeue();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { PriorityQueue } from '@keymanapp/web-utils';
import { LexicalModelTypes } from '@keymanapp/common-types';

import { PathResult, SearchQuotientNode } from './search-quotient-node.js';
import { PathResult } from './correction-searchable.js';
import { SearchQuotientNode } from './search-quotient-node.js';
import { SearchQuotientRoot } from './search-quotient-root.js';
import { QUEUE_NODE_COMPARATOR } from './search-quotient-spur.js';
import { SearchNode } from './distance-modeler.js';
import { TokenResultMapping } from './token-result-mapping.js';

import LexicalModel = LexicalModelTypes.LexicalModel;
import { TokenResultMapping } from './token-result-mapping.js';

export class LegacyQuotientRoot extends SearchQuotientRoot {
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(QUEUE_NODE_COMPARATOR);
Expand All @@ -28,7 +29,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
* sort of result the edge's destination node represents.
* @returns
*/
public handleNextNode(): PathResult {
public handleNextNode(): PathResult<TokenResultMapping> {
const node = this.selectionQueue.dequeue();

if(!node) {
Expand All @@ -47,7 +48,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
return {
type: 'complete',
cost: node.currentCost,
mapping: new TokenResultMapping(node),
mapping: new TokenResultMapping(node, this),
spaceId: this.spaceId
};
}
Expand All @@ -57,7 +58,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
}

get previousResults(): TokenResultMapping[] {
return this.processed.map((n) => new TokenResultMapping(n));
return this.processed.map((n) => new TokenResultMapping(n, this));
}

split(charIndex: number): [SearchQuotientNode, SearchQuotientNode][] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
import { KMWString } from '@keymanapp/web-utils';

import { PathResult } from './correction-searchable.js';
import { SearchNode } from './distance-modeler.js';
import { PathResult, SearchQuotientNode, PathInputProperties } from './search-quotient-node.js';
import { SearchQuotientNode, PathInputProperties } from './search-quotient-node.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';
import { TokenResultMapping } from './token-result-mapping.js';

import Distribution = LexicalModelTypes.Distribution;
import ProbabilityMass = LexicalModelTypes.ProbabilityMass;
Expand Down Expand Up @@ -78,7 +80,7 @@ export class LegacyQuotientSpur extends SearchQuotientSpur {
* sort of result the edge's destination node represents.
* @returns
*/
public handleNextNode(): PathResult {
public handleNextNode(): PathResult<TokenResultMapping> {
const result = super.handleNextNode();

if(result.type == 'complete') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
import { QueueComparator, PriorityQueue } from '@keymanapp/web-utils';
import { LexicalModelTypes } from '@keymanapp/common-types';

import { PathResult } from './correction-searchable.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 { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';
import { TokenResultMapping } from './token-result-mapping.js';

Expand Down Expand Up @@ -138,7 +139,7 @@ export class SearchQuotientCluster implements SearchQuotientNode {
* sort of result the edge's destination node represents.
* @returns
*/
public handleNextNode(): PathResult {
public handleNextNode(): PathResult<TokenResultMapping> {
const bestPath = this.selectionQueue.dequeue();
const currentResult = bestPath.handleNextNode();
this.selectionQueue.enqueue(bestPath);
Expand All @@ -152,7 +153,7 @@ export class SearchQuotientCluster implements SearchQuotientNode {
}

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

get model(): LexicalModelTypes.LexicalModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

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

import { CorrectionSearchable, PathResult } from "./correction-searchable.js";
import { SearchNode } from "./distance-modeler.js";
import { TokenResultMapping } from "./token-result-mapping.js";

import LexicalModel = LexicalModelTypes.LexicalModel;
Expand All @@ -19,24 +21,6 @@ export function generateSpaceSeed(): number {
return SPACE_ID_SEED++;
}

type NullPath = {
type: 'none'
}

type IntermediateSearchPath = {
type: 'intermediate',
cost: number
}

type CompleteSearchPath = {
type: 'complete',
cost: number,
mapping: TokenResultMapping,
spaceId: number
}

export type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath;

export interface InputSegment {
/**
* The transform / transition ID of the corresponding input event.
Expand Down Expand Up @@ -96,7 +80,7 @@ export interface PathInputProperties {
* Represents all or a portion of the dynamically-generated graph used to search
* for predictive-text corrections.
*/
export interface SearchQuotientNode {
export interface SearchQuotientNode extends CorrectionSearchable<SearchNode, TokenResultMapping> {
/**
* Returns an identifier uniquely identifying this search-batching structure
* by correction-search results.
Expand All @@ -120,7 +104,7 @@ export interface SearchQuotientNode {
* what sort of result the edge's destination node represents.
* @returns
*/
handleNextNode(): PathResult;
handleNextNode(): PathResult<TokenResultMapping>;

/**
* Increases the editing range that will be considered for determining
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@

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

import { PathResult } from './correction-searchable.js';
import { SearchNode } from './distance-modeler.js';
import { generateSpaceSeed, InputSegment, PathResult, SearchQuotientNode } from './search-quotient-node.js';
import { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';
import { TokenResultMapping } from './token-result-mapping.js';

Expand Down Expand Up @@ -36,7 +37,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {

this.rootNode = new SearchNode(model.traverseFromRoot(), generateSpaceSeed(), t => model.toKey(t));
this.model = model;
this.rootResult = new TokenResultMapping(this.rootNode);
this.rootResult = new TokenResultMapping(this.rootNode, this);
}

get spaceId(): number {
Expand Down Expand Up @@ -67,7 +68,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {
* sort of result the edge's destination node represents.
* @returns
*/
public handleNextNode(): PathResult {
public handleNextNode(): PathResult<TokenResultMapping> {
if(this.hasBeenProcessed) {
return { type: 'none' };
}
Expand All @@ -77,7 +78,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {
return {
type: 'complete',
cost: 0,
mapping: new TokenResultMapping(this.rootNode),
mapping: new TokenResultMapping(this.rootNode, this),
spaceId: this.spaceId
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { QueueComparator, KMWString, PriorityQueue } from '@keymanapp/web-utils'
import { LexicalModelTypes } from '@keymanapp/common-types';
import { buildMergedTransform } from '@keymanapp/models-templates';

import { PathResult } from './correction-searchable.js';
import { EDIT_DISTANCE_COST_SCALE, SearchNode } from './distance-modeler.js';
import { generateSpaceSeed, InputSegment, PathInputProperties, PathResult, SearchQuotientNode } from './search-quotient-node.js';
import { generateSpaceSeed, InputSegment, PathInputProperties, SearchQuotientNode } from './search-quotient-node.js';
import { generateSubsetId } from './tokenization-subsets.js';
import { SearchQuotientRoot } from './search-quotient-root.js';
import { LegacyQuotientRoot } from './legacy-quotient-root.js';
Expand Down Expand Up @@ -332,7 +333,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
* sort of result the edge's destination node represents.
* @returns
*/
public handleNextNode(): PathResult {
public handleNextNode(): PathResult<TokenResultMapping> {
const parentCost = this.parentNode?.currentCost ?? Number.POSITIVE_INFINITY;
const localCost = this.selectionQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY;

Expand All @@ -353,13 +354,13 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
return {
...result,
type: 'intermediate'
} as PathResult
} as PathResult<TokenResultMapping>
}

// will have equal .spaceId.
let currentNode = this.selectionQueue.dequeue();

let unmatchedResult: PathResult = {
let unmatchedResult: PathResult<TokenResultMapping> = {
type: 'intermediate',
cost: currentNode.currentCost
}
Expand Down Expand Up @@ -404,7 +405,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
return {
type: 'complete',
cost: currentNode.currentCost,
mapping: new TokenResultMapping(currentNode),
mapping: new TokenResultMapping(currentNode, this),
spaceId: this.spaceId
};
}
Expand All @@ -414,7 +415,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
}

public get previousResults(): TokenResultMapping[] {
return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(v));
return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(v, this));
}

public get inputSegments(): InputSegment[] {
Expand Down
Loading
Loading