@@ -7,11 +7,13 @@ import {
7
7
ITermToRelatedTermsIndex ,
8
8
ITermToSemanticRefIndex ,
9
9
KnowledgeType ,
10
+ QueryTerm ,
10
11
ScoredSemanticRef ,
11
12
SemanticRef ,
12
13
SemanticRefIndex ,
13
14
Term ,
14
15
} from "./dataFormat.js" ;
16
+ import * as knowLib from "knowledge-processor" ;
15
17
16
18
export function isConversationSearchable ( conversation : IConversation ) : boolean {
17
19
return (
@@ -26,14 +28,6 @@ export interface IQueryOpExpr<T> {
26
28
eval ( context : QueryEvalContext ) : Promise < T > ;
27
29
}
28
30
29
- export type QueryTerm = {
30
- term : Term ;
31
- /**
32
- * These can be supplied from fuzzy synonym tables and so on
33
- */
34
- relatedTerms ?: Term [ ] | undefined ;
35
- } ;
36
-
37
31
export class QueryEvalContext {
38
32
constructor ( private conversation : IConversation ) {
39
33
if ( ! isConversationSearchable ( conversation ) ) {
@@ -69,17 +63,14 @@ export class SelectTopNExpr<T extends MatchAccumulator>
69
63
}
70
64
71
65
export class TermsMatchExpr implements IQueryOpExpr < SemanticRefAccumulator > {
72
- constructor (
73
- public terms : IQueryOpExpr < QueryTerm [ ] > ,
74
- public semanticRefIndex ?: ITermToSemanticRefIndex ,
75
- ) { }
66
+ constructor ( public terms : IQueryOpExpr < QueryTerm [ ] > ) { }
76
67
77
68
public async eval (
78
69
context : QueryEvalContext ,
79
70
) : Promise < SemanticRefAccumulator > {
80
71
const matchAccumulator : SemanticRefAccumulator =
81
72
new SemanticRefAccumulator ( ) ;
82
- const index = this . semanticRefIndex ?? context . semanticRefIndex ;
73
+ const index = context . semanticRefIndex ;
83
74
const terms = await this . terms . eval ( context ) ;
84
75
for ( const queryTerm of terms ) {
85
76
this . accumulateMatches ( index , matchAccumulator , queryTerm ) ;
@@ -102,6 +93,7 @@ export class TermsMatchExpr implements IQueryOpExpr<SemanticRefAccumulator> {
102
93
// BUT are scored with the score of the related term
103
94
matchAccumulator . addRelatedTermMatch (
104
95
queryTerm . term ,
96
+ relatedTerm ,
105
97
index . lookupTerm ( relatedTerm . text ) ,
106
98
relatedTerm . score ,
107
99
) ;
@@ -111,14 +103,11 @@ export class TermsMatchExpr implements IQueryOpExpr<SemanticRefAccumulator> {
111
103
}
112
104
113
105
export class ResolveRelatedTermsExpr implements IQueryOpExpr < QueryTerm [ ] > {
114
- constructor (
115
- public terms : IQueryOpExpr < QueryTerm [ ] > ,
116
- public index ?: ITermToRelatedTermsIndex ,
117
- ) { }
106
+ constructor ( public terms : IQueryOpExpr < QueryTerm [ ] > ) { }
118
107
119
108
public async eval ( context : QueryEvalContext ) : Promise < QueryTerm [ ] > {
120
109
const terms = await this . terms . eval ( context ) ;
121
- const index = this . index ?? context . relatedTermIndex ;
110
+ const index = context . relatedTermIndex ;
122
111
if ( index !== undefined ) {
123
112
for ( const queryTerm of terms ) {
124
113
if (
@@ -184,6 +173,113 @@ export class SelectTopNKnowledgeGroupExpr
184
173
}
185
174
}
186
175
176
+ export class WhereSemanticRefExpr
177
+ implements IQueryOpExpr < SemanticRefAccumulator >
178
+ {
179
+ constructor (
180
+ public sourceExpr : IQueryOpExpr < SemanticRefAccumulator > ,
181
+ public predicates : IQueryOpPredicate [ ] ,
182
+ ) { }
183
+
184
+ public async eval (
185
+ context : QueryEvalContext ,
186
+ ) : Promise < SemanticRefAccumulator > {
187
+ const accumulator = await this . sourceExpr . eval ( context ) ;
188
+ const filtered = new SemanticRefAccumulator (
189
+ accumulator . queryTermMatches ,
190
+ ) ;
191
+ const semanticRefs = context . semanticRefs ;
192
+ filtered . setMatches (
193
+ accumulator . getMatchesWhere ( ( match ) =>
194
+ this . testOr ( semanticRefs , accumulator . queryTermMatches , match ) ,
195
+ ) ,
196
+ ) ;
197
+ return filtered ;
198
+ }
199
+
200
+ private testOr (
201
+ semanticRefs : SemanticRef [ ] ,
202
+ queryTermMatches : QueryTermAccumulator ,
203
+ match : Match < SemanticRefIndex > ,
204
+ ) {
205
+ for ( let i = 0 ; i < this . predicates . length ; ++ i ) {
206
+ const semanticRef = semanticRefs [ match . value ] ;
207
+ if ( this . predicates [ i ] . eval ( queryTermMatches , semanticRef ) ) {
208
+ return true ;
209
+ }
210
+ }
211
+ return false ;
212
+ }
213
+ }
214
+
215
+ export interface IQueryOpPredicate {
216
+ eval ( termMatches : QueryTermAccumulator , semanticRef : SemanticRef ) : boolean ;
217
+ }
218
+
219
+ export class EntityPredicate implements IQueryOpPredicate {
220
+ constructor (
221
+ public type : string | undefined ,
222
+ public name : string | undefined ,
223
+ public facetName : string | undefined ,
224
+ ) { }
225
+
226
+ public eval (
227
+ termMatches : QueryTermAccumulator ,
228
+ semanticRef : SemanticRef ,
229
+ ) : boolean {
230
+ if ( semanticRef . knowledgeType !== "entity" ) {
231
+ return false ;
232
+ }
233
+ const entity =
234
+ semanticRef . knowledge as knowLib . conversation . ConcreteEntity ;
235
+ return (
236
+ termMatches . matched ( entity . type , this . type ) &&
237
+ termMatches . matched ( entity . name , this . name ) &&
238
+ this . matchFacet ( termMatches , entity , this . facetName )
239
+ ) ;
240
+ }
241
+
242
+ private matchFacet (
243
+ termMatches : QueryTermAccumulator ,
244
+ entity : knowLib . conversation . ConcreteEntity ,
245
+ facetName ?: string | undefined ,
246
+ ) : boolean {
247
+ if ( facetName === undefined || entity . facets === undefined ) {
248
+ return false ;
249
+ }
250
+ for ( const facet of entity . facets ) {
251
+ if ( termMatches . matched ( facet . name , facetName ) ) {
252
+ return true ;
253
+ }
254
+ }
255
+ return false ;
256
+ }
257
+ }
258
+
259
+ export class ActionPredicate implements IQueryOpPredicate {
260
+ constructor (
261
+ public subjectEntityName ?: string | undefined ,
262
+ public objectEntityName ?: string | undefined ,
263
+ ) { }
264
+
265
+ public eval (
266
+ termMatches : QueryTermAccumulator ,
267
+ semanticRef : SemanticRef ,
268
+ ) : boolean {
269
+ if ( semanticRef . knowledgeType !== "action" ) {
270
+ return false ;
271
+ }
272
+ const action = semanticRef . knowledge as knowLib . conversation . Action ;
273
+ return (
274
+ termMatches . matched (
275
+ action . subjectEntityName ,
276
+ this . subjectEntityName ,
277
+ ) &&
278
+ termMatches . matched ( action . objectEntityName , this . objectEntityName )
279
+ ) ;
280
+ }
281
+ }
282
+
187
283
export interface Match < T = any > {
188
284
value : T ;
189
285
score : number ;
@@ -335,7 +431,7 @@ export class MatchAccumulator<T = any> {
335
431
}
336
432
337
433
export class SemanticRefAccumulator extends MatchAccumulator < SemanticRefIndex > {
338
- constructor ( public termMatches : Set < string > = new Set < string > ( ) ) {
434
+ constructor ( public queryTermMatches = new QueryTermAccumulator ( ) ) {
339
435
super ( ) ;
340
436
}
341
437
@@ -349,17 +445,20 @@ export class SemanticRefAccumulator extends MatchAccumulator<SemanticRefIndex> {
349
445
for ( const match of semanticRefs ) {
350
446
this . add ( match . semanticRefIndex , match . score + scoreBoost ) ;
351
447
}
352
- this . recordTermMatch ( term . text ) ;
448
+ this . queryTermMatches . add ( term ) ;
353
449
}
354
450
}
355
451
356
452
public addRelatedTermMatch (
357
- term : Term ,
453
+ primaryTerm : Term ,
454
+ relatedTerm : Term ,
358
455
semanticRefs : ScoredSemanticRef [ ] | undefined ,
359
456
scoreBoost ?: number ,
360
457
) {
361
458
if ( semanticRefs ) {
362
- scoreBoost ??= term . score ?? 0 ;
459
+ // Related term matches count as matches for the queryTerm...
460
+ // BUT are scored with the score of the related term
461
+ scoreBoost ??= relatedTerm . score ?? 0 ;
363
462
for ( const semanticRef of semanticRefs ) {
364
463
let score = semanticRef . score + scoreBoost ;
365
464
let match = this . getMatch ( semanticRef . semanticRefIndex ) ;
@@ -376,14 +475,10 @@ export class SemanticRefAccumulator extends MatchAccumulator<SemanticRefIndex> {
376
475
this . setMatch ( match ) ;
377
476
}
378
477
}
379
- this . recordTermMatch ( term . text ) ;
478
+ this . queryTermMatches . add ( primaryTerm , relatedTerm ) ;
380
479
}
381
480
}
382
481
383
- public recordTermMatch ( term : string ) {
384
- this . termMatches . add ( term ) ;
385
- }
386
-
387
482
public override getSortedByScore (
388
483
minHitCount ?: number ,
389
484
) : Match < SemanticRefIndex > [ ] {
@@ -409,7 +504,7 @@ export class SemanticRefAccumulator extends MatchAccumulator<SemanticRefIndex> {
409
504
let group = groups . get ( semanticRef . knowledgeType ) ;
410
505
if ( group === undefined ) {
411
506
group = new SemanticRefAccumulator ( ) ;
412
- group . termMatches = this . termMatches ;
507
+ group . queryTermMatches = this . queryTermMatches ;
413
508
groups . set ( semanticRef . knowledgeType , group ) ;
414
509
}
415
510
group . setMatch ( match ) ;
@@ -427,6 +522,68 @@ export class SemanticRefAccumulator extends MatchAccumulator<SemanticRefIndex> {
427
522
}
428
523
429
524
private getMinHitCount ( minHitCount ?: number ) : number {
430
- return minHitCount !== undefined ? minHitCount : this . termMatches . size ;
525
+ return minHitCount !== undefined
526
+ ? minHitCount
527
+ : this . queryTermMatches . termMatches . size ;
528
+ }
529
+ }
530
+
531
+ export class QueryTermAccumulator {
532
+ constructor (
533
+ public termMatches : Set < string > = new Set < string > ( ) ,
534
+ public relatedTermToTerms : Map < string , Set < string > > = new Map <
535
+ string ,
536
+ Set < string >
537
+ > ( ) ,
538
+ ) { }
539
+
540
+ public add ( term : Term , relatedTerm ?: Term ) {
541
+ this . termMatches . add ( term . text ) ;
542
+ if ( relatedTerm !== undefined ) {
543
+ let relatedTermToTerms = this . relatedTermToTerms . get (
544
+ relatedTerm . text ,
545
+ ) ;
546
+ if ( relatedTermToTerms === undefined ) {
547
+ relatedTermToTerms = new Set < string > ( ) ;
548
+ this . relatedTermToTerms . set (
549
+ relatedTerm . text ,
550
+ relatedTermToTerms ,
551
+ ) ;
552
+ }
553
+ relatedTermToTerms . add ( term . text ) ;
554
+ }
555
+ }
556
+
557
+ public matched (
558
+ testText : string | string [ ] | undefined ,
559
+ expectedText : string | undefined ,
560
+ ) : boolean {
561
+ if ( expectedText === undefined ) {
562
+ return true ;
563
+ }
564
+ if ( testText === undefined ) {
565
+ return false ;
566
+ }
567
+
568
+ if ( Array . isArray ( testText ) ) {
569
+ for ( const text of testText ) {
570
+ if ( this . matched ( text , expectedText ) ) {
571
+ return true ;
572
+ }
573
+ }
574
+ return false ;
575
+ }
576
+
577
+ if ( testText === expectedText ) {
578
+ return true ;
579
+ }
580
+
581
+ // Maybe the test text matched a related term.
582
+ // If so, the matching related term should have matched on behalf of
583
+ // of a term === expectedTerm
584
+ const relatedTermToTerms = this . relatedTermToTerms . get ( testText ) ;
585
+ return relatedTermToTerms !== undefined
586
+ ? relatedTermToTerms . has ( expectedText )
587
+ : false ;
431
588
}
432
589
}
0 commit comments