mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-20 12:57:49 +00:00
Merge branch 'refactor/web/extract-duplicate-result-filter-extended' into feat/web/correction-search-abstraction-extended
This commit is contained in:
commit
a4f392fb2a
9 changed files with 70 additions and 41 deletions
|
|
@ -48,7 +48,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
|
|||
return {
|
||||
type: 'complete',
|
||||
cost: node.currentCost,
|
||||
mapping: new TokenResultMapping(node, this)
|
||||
mapping: new TokenResultMapping(this, node)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
|
|||
}
|
||||
|
||||
get previousResults(): TokenResultMapping[] {
|
||||
return this.processed.map((n) => new TokenResultMapping(n, this));
|
||||
return this.processed.map((n) => new TokenResultMapping(this, n));
|
||||
}
|
||||
|
||||
split(charIndex: number): [SearchQuotientNode, SearchQuotientNode][] {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export class LegacyQuotientSpur extends SearchQuotientSpur {
|
|||
const codepointLength = space.codepointLength + insertLength - inputSample.deleteLeft;
|
||||
|
||||
super(space, inputs, inputSource, codepointLength);
|
||||
this.queueNodes(this.buildEdgesForNodes(space.previousResults.map(r => r.node)));
|
||||
this.queueNodes(this.buildEdgesFromResults(space.previousResults));
|
||||
this.insertLength = insertLength;
|
||||
this.leftDeleteLength = inputSample.deleteLeft;
|
||||
return;
|
||||
|
|
@ -52,19 +52,19 @@ export class LegacyQuotientSpur extends SearchQuotientSpur {
|
|||
return new LegacyQuotientSpur(parentNode, inputs, inputSource) as this;
|
||||
}
|
||||
|
||||
protected buildEdgesForNodes(baseNodes: ReadonlyArray<SearchNode>) {
|
||||
protected buildEdgesFromResults(priorResults: ReadonlyArray<TokenResultMapping>) {
|
||||
// With a newly-available input, we can extend new input-dependent paths from
|
||||
// our previously-reached 'extractedResults' nodes.
|
||||
let outboundNodes = baseNodes.map((node) => {
|
||||
let outboundNodes = priorResults.map((result) => {
|
||||
// Hard restriction: no further edits will be supported. This helps keep the search
|
||||
// more narrowly focused.
|
||||
const substitutionsOnly = node.editCount == 2;
|
||||
const substitutionsOnly = result.editCount == 2;
|
||||
|
||||
let deletionEdges: SearchNode[] = [];
|
||||
if(!substitutionsOnly) {
|
||||
deletionEdges = node.buildDeletionEdges(this.inputs, this.spaceId);
|
||||
deletionEdges = result.buildDeletionEdges(this.inputs, this.spaceId);
|
||||
}
|
||||
const substitutionEdges = node.buildSubstitutionEdges(this.inputs, this.spaceId);
|
||||
const substitutionEdges = result.buildSubstitutionEdges(this.inputs, this.spaceId);
|
||||
|
||||
// Skip the queue for the first pass; there will ALWAYS be at least one pass,
|
||||
// and queue-enqueing does come with a cost - avoid unnecessary overhead here.
|
||||
|
|
@ -84,12 +84,12 @@ export class LegacyQuotientSpur extends SearchQuotientSpur {
|
|||
const result = super.handleNextNode();
|
||||
|
||||
if(result.type == 'complete') {
|
||||
const currentNode = result.mapping.node;
|
||||
const parentResult = result.mapping;
|
||||
|
||||
// Forbid a raw edit-distance of greater than 2.
|
||||
// Note: .knownCost is not scaled, while its contribution to .currentCost _is_ scaled.
|
||||
if(currentNode.editCount < 2) {
|
||||
let insertionEdges = currentNode.buildInsertionEdges();
|
||||
if(parentResult.editCount < 2) {
|
||||
let insertionEdges = parentResult.buildInsertionEdges();
|
||||
this.queueNodes(insertionEdges);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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, SearchQuotientNode } from './search-quotient-node.js';
|
||||
import { SearchQuotientSpur } from './search-quotient-spur.js';
|
||||
|
|
@ -37,7 +36,7 @@ export class SearchQuotientCluster implements SearchQuotientNode {
|
|||
* 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[] = [];
|
||||
private completedPaths?: TokenResultMapping[] = [];
|
||||
|
||||
/**
|
||||
* Acts as a Map that prevents duplicating a correction-search path if reached
|
||||
|
|
@ -87,8 +86,7 @@ export class SearchQuotientCluster implements SearchQuotientNode {
|
|||
this.spaceId = generateSpaceSeed();
|
||||
|
||||
this.lowestPossibleSingleCost = lowestPossibleSingleCost;
|
||||
this.completedPaths = inboundPaths.flatMap(p => p.previousResults).map(r => r.node);
|
||||
this.completedPaths.forEach((p) => p.spaceId = this.spaceId);
|
||||
this.completedPaths = inboundPaths.flatMap(p => p.previousResults).map(r => new TokenResultMapping(this, r));
|
||||
this.selectionQueue.enqueueAll(inboundPaths);
|
||||
|
||||
return;
|
||||
|
|
@ -146,16 +144,14 @@ export class SearchQuotientCluster implements SearchQuotientNode {
|
|||
this.selectionQueue.enqueue(bestPath);
|
||||
|
||||
if(currentResult.type == 'complete') {
|
||||
const node = currentResult.mapping.node;
|
||||
node.spaceId = this.spaceId;
|
||||
this.completedPaths?.push(node);
|
||||
this.completedPaths.push(new TokenResultMapping(this, currentResult.mapping));
|
||||
}
|
||||
|
||||
return currentResult;
|
||||
}
|
||||
|
||||
public get previousResults(): TokenResultMapping[] {
|
||||
return this.completedPaths?.map((n) => new TokenResultMapping(n, this)) ?? [];
|
||||
return this.completedPaths;
|
||||
}
|
||||
|
||||
get model(): LexicalModelTypes.LexicalModel {
|
||||
|
|
|
|||
|
|
@ -37,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);
|
||||
this.rootResult = new TokenResultMapping(this, this.rootNode);
|
||||
}
|
||||
|
||||
get spaceId(): number {
|
||||
|
|
@ -78,7 +78,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {
|
|||
return {
|
||||
type: 'complete',
|
||||
cost: 0,
|
||||
mapping: new TokenResultMapping(this.rootNode, this)
|
||||
mapping: new TokenResultMapping(this, this.rootNode)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
|
|||
return Math.min(localCost, parentCost);
|
||||
}
|
||||
|
||||
protected abstract buildEdgesForNodes(baseNodes: ReadonlyArray<SearchNode>): SearchNode[];
|
||||
protected abstract buildEdgesFromResults(baseNodes: ReadonlyArray<TokenResultMapping>): SearchNode[];
|
||||
|
||||
protected queueNodes(nodes: SearchNode[]) {
|
||||
this.selectionQueue.enqueueAll(nodes);
|
||||
|
|
@ -348,7 +348,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
|
|||
const result = this.parentNode.handleNextNode();
|
||||
|
||||
if(result.type == 'complete') {
|
||||
this.queueNodes(this.buildEdgesForNodes([result.mapping.node]));
|
||||
this.queueNodes(this.buildEdgesFromResults([result.mapping]));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -405,7 +405,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
|
|||
return {
|
||||
type: 'complete',
|
||||
cost: currentNode.currentCost,
|
||||
mapping: new TokenResultMapping(currentNode, this)
|
||||
mapping: new TokenResultMapping(this, currentNode)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
|
|||
}
|
||||
|
||||
public get previousResults(): TokenResultMapping[] {
|
||||
return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(v, this));
|
||||
return Object.values(this.returnedValues ?? {}).map(v => new TokenResultMapping(this, v));
|
||||
}
|
||||
|
||||
public get inputSegments(): InputSegment[] {
|
||||
|
|
|
|||
|
|
@ -10,29 +10,27 @@
|
|||
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { CorrectionResultMapping } from "./correction-result-mapping.js";
|
||||
import { SearchNode, TraversableToken } from "./distance-modeler.js";
|
||||
import { SearchQuotientNode } from "./search-quotient-node.js";
|
||||
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import LexiconTraversal = LexicalModelTypes.LexiconTraversal;
|
||||
import ProbabilityMass = LexicalModelTypes.ProbabilityMass;
|
||||
import Transform = LexicalModelTypes.Transform;
|
||||
|
||||
export function initTokenResultFilterer() {
|
||||
const priorReturns: Map<string, SearchNode> = new Map();
|
||||
const priorReturns: Map<string, TokenResultMapping> = new Map();
|
||||
|
||||
const closure = (searchResult: TokenResultMapping) => {
|
||||
const node = searchResult.node;
|
||||
|
||||
if(node.isFullReplacement) {
|
||||
if(searchResult.isFullReplacement) {
|
||||
// If the entry's 'match' fully replaces the input string, we consider it
|
||||
// unreasonable and ignore it. Also, if we've reached this point...
|
||||
// we can(?) assume that everything thereafter is as well.
|
||||
return false;
|
||||
}
|
||||
|
||||
if((priorReturns.get(node.resultKey)?.currentCost ?? Number.MAX_VALUE) > searchResult.totalCost) {
|
||||
priorReturns.set(node.resultKey, node);
|
||||
if((priorReturns.get(searchResult.matchString)?.totalCost ?? Number.MAX_VALUE) > searchResult.totalCost) {
|
||||
priorReturns.set(searchResult.matchString, searchResult);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
|
|
@ -43,13 +41,29 @@ export function initTokenResultFilterer() {
|
|||
return closure;
|
||||
}
|
||||
|
||||
export class TokenResultMapping implements CorrectionResultMapping<SearchNode> {
|
||||
export class TokenResultMapping {
|
||||
readonly matchingSpace: SearchQuotientNode;
|
||||
readonly node: SearchNode;
|
||||
private readonly node: SearchNode;
|
||||
readonly spaceId: number;
|
||||
|
||||
constructor(node: SearchNode, finalQuotientNode: SearchQuotientNode) {
|
||||
constructor(finalQuotientNode: SearchQuotientNode, node: SearchNode);
|
||||
constructor(finalQuotientNode: SearchQuotientNode, mapping: TokenResultMapping);
|
||||
constructor(finalQuotientNode: SearchQuotientNode, mappingBase: SearchNode | TokenResultMapping) {
|
||||
if(!mappingBase) {
|
||||
throw new Error("Result-mapping parameters may not be null");
|
||||
}
|
||||
this.matchingSpace = finalQuotientNode;
|
||||
this.node = node;
|
||||
|
||||
if(mappingBase instanceof SearchNode) {
|
||||
this.node = mappingBase;
|
||||
this.spaceId = mappingBase.spaceId;
|
||||
if(mappingBase.spaceId != this.matchingSpace.spaceId) {
|
||||
throw new Error("QuotientNode and SearchNode .spaceId values provided to TokenResultMapping constructor should be equal");
|
||||
}
|
||||
} else {
|
||||
this.node = mappingBase.node;
|
||||
this.spaceId = this.matchingSpace.spaceId;
|
||||
}
|
||||
}
|
||||
|
||||
get matchedResult(): Readonly<SearchNode> {
|
||||
|
|
@ -68,6 +82,13 @@ export class TokenResultMapping implements CorrectionResultMapping<SearchNode> {
|
|||
return this.node.resultKey;
|
||||
}
|
||||
|
||||
get editCount(): number {
|
||||
return this.node.editCount;
|
||||
}
|
||||
|
||||
get isFullReplacement(): boolean {
|
||||
return this.node.isFullReplacement;
|
||||
}
|
||||
/**
|
||||
* Gets the number of Damerau-Levenshtein edits needed to reach the node's
|
||||
* matchString from the output induced by the input sequence used to reach it.
|
||||
|
|
@ -100,4 +121,16 @@ export class TokenResultMapping implements CorrectionResultMapping<SearchNode> {
|
|||
get finalTraversal(): LexiconTraversal {
|
||||
return this.node.currentTraversal;
|
||||
}
|
||||
|
||||
buildInsertionEdges(): SearchNode[] {
|
||||
return this.node.buildInsertionEdges();
|
||||
}
|
||||
|
||||
buildDeletionEdges(dist: Distribution<Transform>, edgeId: number): SearchNode[] {
|
||||
return this.node.buildDeletionEdges(dist, edgeId);
|
||||
}
|
||||
|
||||
buildSubstitutionEdges(dist: Distribution<Transform>, edgeId: number): SearchNode[] {
|
||||
return this.node.buildSubstitutionEdges(dist, edgeId);
|
||||
}
|
||||
}
|
||||
|
|
@ -394,7 +394,7 @@ export function determineSuggestionAlignment(
|
|||
export function buildAndMapPredictions(
|
||||
transition: ContextTransition,
|
||||
tokenization: ContextTokenization,
|
||||
match: TokenResultMapping
|
||||
match: Readonly<TokenResultMapping>
|
||||
): CorrectionPredictionTuple[] {
|
||||
const model = transition.final.model;
|
||||
const searchSpace = tokenization.tail.searchModule;
|
||||
|
|
@ -537,7 +537,7 @@ export async function correctAndEnumerate(
|
|||
const correctionPredictionMap: Record<string, Distribution<Suggestion>> = {};
|
||||
for await(const match of getBestMatches<SearchNode, TokenResultMapping, SearchQuotientNode>(searchModules, timer, initTokenResultFilterer())) {
|
||||
// Corrections obtained: now to predict from them!
|
||||
const tokenization = tokenizations.find(t => t.spaceId == match.node.spaceId);
|
||||
const tokenization = tokenizations.find(t => t.spaceId == match.spaceId);
|
||||
|
||||
// If our 'match' results in fully deleting the new token, reject it and try again.
|
||||
if(match.matchSequence.length == 0 && match.inputSequence.length != 0) {
|
||||
|
|
@ -549,7 +549,7 @@ export async function correctAndEnumerate(
|
|||
continue;
|
||||
}
|
||||
|
||||
if(match.node.editCount > 0 && !searchModules.find(s => s.correctionsEnabled)) {
|
||||
if(match.editCount > 0 && !searchModules.find(s => s.correctionsEnabled)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function analyzeQuotientNodeResults(
|
|||
let result: PathResult<TokenResultMapping> = node.handleNextNode();
|
||||
while(result.type != 'none') {
|
||||
if(result.type == 'complete') {
|
||||
const resultKey = result.mapping.node.resultKey;
|
||||
const resultKey = result.mapping.matchString;
|
||||
if(expectedResults.find((entry) => entry == resultKey)) {
|
||||
matchMap.set(resultKey, (matchMap.get(resultKey) ?? 0) + 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function buildTestTimer() {
|
|||
}
|
||||
|
||||
describe('getBestMatches', () => {
|
||||
const checkRepeatableResults_teh = async (iter: AsyncGenerator<TokenResultMapping, any, any>) => {
|
||||
const checkRepeatableResults_teh = async (iter: AsyncGenerator<Readonly<TokenResultMapping>, any, any>) => {
|
||||
const firstIterResult = await iter.next(); // {value: <actual value>, done: <iteration complete?>}
|
||||
assert.isFalse(firstIterResult.done);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue