@memberjunction/server 5.23.0 → 5.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +462 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +16584 -13956
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +10 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +97 -13
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +6 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +21 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +73 -33
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +42 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +239 -13
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/package.json +63 -63
- package/src/__tests__/search-knowledge-tags.test.ts +415 -0
- package/src/config.ts +11 -0
- package/src/generated/generated.ts +1807 -1
- package/src/generic/RunViewResolver.ts +1 -0
- package/src/resolvers/AutotagPipelineResolver.ts +99 -10
- package/src/resolvers/FetchEntityVectorsResolver.ts +6 -2
- package/src/resolvers/RunAIAgentResolver.ts +95 -56
- package/src/resolvers/SearchKnowledgeResolver.ts +270 -13
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Float, InputType } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
3
|
import { LogError, LogStatus, Metadata, RunView, UserInfo, ComputeRRF, ScoredCandidate, EntityRecordNameInput, CompositeKey } from '@memberjunction/core';
|
|
4
|
-
import { MJVectorIndexEntity, MJVectorDatabaseEntity } from '@memberjunction/core-entities';
|
|
4
|
+
import { MJVectorIndexEntity, MJVectorDatabaseEntity, KnowledgeHubMetadataEngine } from '@memberjunction/core-entities';
|
|
5
5
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
6
6
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
7
7
|
import { BaseEmbeddings, GetAIAPIKey } from '@memberjunction/ai';
|
|
@@ -59,6 +59,10 @@ export class SearchKnowledgeResultItem {
|
|
|
59
59
|
|
|
60
60
|
@Field()
|
|
61
61
|
MatchedAt: Date;
|
|
62
|
+
|
|
63
|
+
/** Raw vector metadata as JSON string — contains all entity fields stored in the vector DB */
|
|
64
|
+
@Field({ nullable: true })
|
|
65
|
+
RawMetadata?: string;
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
@ObjectType()
|
|
@@ -149,11 +153,16 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
149
153
|
const fusedResults = this.fuseResults(vectorResults, fullTextResults, topK);
|
|
150
154
|
const dedupedResults = this.deduplicateResults(fusedResults);
|
|
151
155
|
|
|
156
|
+
// Exclude Content Items that originated from Entity sources — those entities
|
|
157
|
+
// are already searchable directly via their own vector embeddings, so showing
|
|
158
|
+
// the Content Item shadow result is redundant.
|
|
159
|
+
const withoutEntityContentItems = await this.excludeEntitySourcedContentItems(dedupedResults, currentUser);
|
|
160
|
+
|
|
152
161
|
// Apply minimum score threshold (post-RRF, so fusion can surface cross-source matches first)
|
|
153
162
|
const scoreThreshold = minScore ?? 0;
|
|
154
163
|
const filteredResults = scoreThreshold > 0
|
|
155
|
-
?
|
|
156
|
-
:
|
|
164
|
+
? withoutEntityContentItems.filter(r => r.Score >= scoreThreshold)
|
|
165
|
+
: withoutEntityContentItems;
|
|
157
166
|
LogStatus(`SearchKnowledge: Fuse + dedup + threshold≥${Math.round(scoreThreshold * 100)}% (${dedupedResults.length} → ${filteredResults.length} results): ${Date.now() - t0}ms`);
|
|
158
167
|
|
|
159
168
|
// Enrich with entity icons and record names
|
|
@@ -341,6 +350,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
341
350
|
* Full-text search using the MJCore Metadata.FullTextSearch() method.
|
|
342
351
|
* Delegates to the provider stack which uses database-native FTS
|
|
343
352
|
* (SQL Server FREETEXT, PostgreSQL tsvector) via RunView + UserSearchString.
|
|
353
|
+
* When Tags filters are provided, post-filters results by checking tag associations.
|
|
344
354
|
*/
|
|
345
355
|
private async searchFullText(
|
|
346
356
|
query: string,
|
|
@@ -361,7 +371,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
361
371
|
return [];
|
|
362
372
|
}
|
|
363
373
|
|
|
364
|
-
|
|
374
|
+
const results: SearchKnowledgeResultItem[] = ftsResult.Results.map(r => ({
|
|
365
375
|
ID: `ft-${r.EntityName}-${r.RecordID}`,
|
|
366
376
|
EntityName: r.EntityName,
|
|
367
377
|
RecordID: r.RecordID,
|
|
@@ -370,15 +380,163 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
370
380
|
Snippet: r.Snippet,
|
|
371
381
|
Score: r.Score,
|
|
372
382
|
ScoreBreakdown: { FullText: r.Score },
|
|
373
|
-
Tags: [],
|
|
383
|
+
Tags: [] as string[],
|
|
374
384
|
MatchedAt: new Date()
|
|
375
385
|
}));
|
|
386
|
+
|
|
387
|
+
// Batch-load tags from TaggedItems/ContentItemTags for FTS results
|
|
388
|
+
await this.enrichResultsWithTags(results, md, contextUser);
|
|
389
|
+
|
|
390
|
+
// Apply tag filter: keep only results that have at least one matching tag
|
|
391
|
+
if (filters?.Tags?.length) {
|
|
392
|
+
return this.filterResultsByTags(results, filters.Tags);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return results;
|
|
376
396
|
} catch (error) {
|
|
377
397
|
LogError(`SearchKnowledge: Full-text search error: ${error}`);
|
|
378
398
|
return [];
|
|
379
399
|
}
|
|
380
400
|
}
|
|
381
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Filter results to only include those with at least one tag matching the specified tag list.
|
|
404
|
+
* Uses case-insensitive comparison for robustness.
|
|
405
|
+
*/
|
|
406
|
+
private filterResultsByTags(results: SearchKnowledgeResultItem[], requiredTags: string[]): SearchKnowledgeResultItem[] {
|
|
407
|
+
const lowerTags = new Set(requiredTags.map(t => t.toLowerCase()));
|
|
408
|
+
return results.filter(r =>
|
|
409
|
+
r.Tags.some(t => lowerTags.has(t.toLowerCase()))
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Enrich search results with tags from both the TaggedItems entity (for general entities)
|
|
415
|
+
* and the ContentItemTag entity (for Content Items). Runs both queries in parallel
|
|
416
|
+
* for maximum throughput, then merges the tag sets onto each result.
|
|
417
|
+
*/
|
|
418
|
+
private async enrichResultsWithTags(
|
|
419
|
+
results: SearchKnowledgeResultItem[],
|
|
420
|
+
md: Metadata,
|
|
421
|
+
contextUser: UserInfo
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
if (results.length === 0) return;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
// Separate results into Content Items vs everything else
|
|
427
|
+
const contentItemResults = results.filter(r => r.EntityName === 'MJ: Content Items');
|
|
428
|
+
const generalResults = results.filter(r => r.EntityName !== 'MJ: Content Items');
|
|
429
|
+
|
|
430
|
+
// Run both tag lookups in parallel
|
|
431
|
+
await Promise.all([
|
|
432
|
+
this.loadTaggedItemTags(generalResults, md, contextUser),
|
|
433
|
+
this.loadContentItemTags(contentItemResults, contextUser)
|
|
434
|
+
]);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
LogError(`SearchKnowledge: Error enriching results with tags: ${error}`);
|
|
437
|
+
// Non-fatal — results still usable without tags
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Load tags from the TaggedItems entity for non-Content-Item results.
|
|
443
|
+
* Queries by EntityID + RecordID pairs in a single batch.
|
|
444
|
+
*/
|
|
445
|
+
private async loadTaggedItemTags(
|
|
446
|
+
results: SearchKnowledgeResultItem[],
|
|
447
|
+
md: Metadata,
|
|
448
|
+
contextUser: UserInfo
|
|
449
|
+
): Promise<void> {
|
|
450
|
+
if (results.length === 0) return;
|
|
451
|
+
|
|
452
|
+
// Get entity IDs from metadata
|
|
453
|
+
const entityIdMap = new Map<string, string>();
|
|
454
|
+
for (const r of results) {
|
|
455
|
+
if (!entityIdMap.has(r.EntityName)) {
|
|
456
|
+
const entityInfo = md.Entities.find(e => e.Name === r.EntityName);
|
|
457
|
+
if (entityInfo) {
|
|
458
|
+
entityIdMap.set(r.EntityName, entityInfo.ID);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (entityIdMap.size === 0) return;
|
|
464
|
+
|
|
465
|
+
// Build filter for TaggedItems: OR across all entity+record pairs
|
|
466
|
+
const conditions: string[] = [];
|
|
467
|
+
for (const r of results) {
|
|
468
|
+
const entityID = entityIdMap.get(r.EntityName);
|
|
469
|
+
if (!entityID) continue;
|
|
470
|
+
conditions.push(`(EntityID='${entityID}' AND RecordID='${r.RecordID}')`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (conditions.length === 0) return;
|
|
474
|
+
|
|
475
|
+
const rv = new RunView();
|
|
476
|
+
const tagResult = await rv.RunView<{ EntityID: string; RecordID: string; Tag: string }>({
|
|
477
|
+
EntityName: 'MJ: Tagged Items',
|
|
478
|
+
ExtraFilter: conditions.join(' OR '),
|
|
479
|
+
Fields: ['EntityID', 'RecordID', 'Tag'],
|
|
480
|
+
ResultType: 'simple'
|
|
481
|
+
}, contextUser);
|
|
482
|
+
|
|
483
|
+
if (!tagResult.Success) return;
|
|
484
|
+
|
|
485
|
+
// Build a lookup: "entityID::recordID" -> tag names
|
|
486
|
+
const tagMap = new Map<string, string[]>();
|
|
487
|
+
for (const ti of tagResult.Results) {
|
|
488
|
+
const key = `${ti.EntityID}::${ti.RecordID}`;
|
|
489
|
+
const tags = tagMap.get(key) ?? [];
|
|
490
|
+
tags.push(ti.Tag);
|
|
491
|
+
tagMap.set(key, tags);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Apply tags to results
|
|
495
|
+
for (const r of results) {
|
|
496
|
+
const entityID = entityIdMap.get(r.EntityName);
|
|
497
|
+
if (!entityID) continue;
|
|
498
|
+
const key = `${entityID}::${r.RecordID}`;
|
|
499
|
+
r.Tags = tagMap.get(key) ?? [];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Load tags from the ContentItemTag entity for Content Item results.
|
|
505
|
+
* Queries by ItemID (which maps to the result's RecordID) in a single batch.
|
|
506
|
+
*/
|
|
507
|
+
private async loadContentItemTags(
|
|
508
|
+
results: SearchKnowledgeResultItem[],
|
|
509
|
+
contextUser: UserInfo
|
|
510
|
+
): Promise<void> {
|
|
511
|
+
if (results.length === 0) return;
|
|
512
|
+
|
|
513
|
+
const recordIDs = results.map(r => `'${r.RecordID}'`);
|
|
514
|
+
const rv = new RunView();
|
|
515
|
+
const tagResult = await rv.RunView<{ ItemID: string; Tag: string }>({
|
|
516
|
+
EntityName: 'MJ: Content Item Tags',
|
|
517
|
+
ExtraFilter: `ItemID IN (${recordIDs.join(',')})`,
|
|
518
|
+
Fields: ['ItemID', 'Tag'],
|
|
519
|
+
ResultType: 'simple'
|
|
520
|
+
}, contextUser);
|
|
521
|
+
|
|
522
|
+
if (!tagResult.Success) return;
|
|
523
|
+
|
|
524
|
+
// Build a lookup: ItemID -> tag names
|
|
525
|
+
const tagMap = new Map<string, string[]>();
|
|
526
|
+
for (const ti of tagResult.Results) {
|
|
527
|
+
const key = ti.ItemID;
|
|
528
|
+
const tags = tagMap.get(key) ?? [];
|
|
529
|
+
tags.push(ti.Tag);
|
|
530
|
+
tagMap.set(key, tags);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Apply tags to results (use case-insensitive UUID comparison)
|
|
534
|
+
for (const r of results) {
|
|
535
|
+
// Check both raw and lowercase keys since UUID casing may vary
|
|
536
|
+
r.Tags = tagMap.get(r.RecordID) ?? tagMap.get(r.RecordID.toLowerCase()) ?? [];
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
382
540
|
/**
|
|
383
541
|
* Fuse vector and full-text results using Reciprocal Rank Fusion (RRF).
|
|
384
542
|
* Deduplicates by RecordID, preferring the higher-scored source.
|
|
@@ -392,9 +550,14 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
392
550
|
return [];
|
|
393
551
|
}
|
|
394
552
|
|
|
395
|
-
// If only one source has results,
|
|
396
|
-
|
|
397
|
-
if (
|
|
553
|
+
// If only one source has results, normalize scores relative to the top result
|
|
554
|
+
// so the best match shows ~90-95% instead of raw cosine similarity (~40-50%)
|
|
555
|
+
if (fullTextResults.length === 0) {
|
|
556
|
+
return this.normalizeScores(vectorResults.slice(0, topK));
|
|
557
|
+
}
|
|
558
|
+
if (vectorResults.length === 0) {
|
|
559
|
+
return this.normalizeScores(fullTextResults.slice(0, topK));
|
|
560
|
+
}
|
|
398
561
|
|
|
399
562
|
// Build scored candidate lists for RRF
|
|
400
563
|
const vectorCandidates: ScoredCandidate[] = vectorResults.map((r, i) => ({
|
|
@@ -441,6 +604,26 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
441
604
|
});
|
|
442
605
|
}
|
|
443
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Normalize scores when only one search source returned results.
|
|
609
|
+
* Scales scores relative to the top result so the best match shows
|
|
610
|
+
* ~90-95% instead of raw cosine similarity (~40-50%).
|
|
611
|
+
* This prevents artificially low-looking scores when RRF isn't applied.
|
|
612
|
+
*/
|
|
613
|
+
private normalizeScores(results: SearchKnowledgeResultItem[]): SearchKnowledgeResultItem[] {
|
|
614
|
+
if (results.length === 0) return results;
|
|
615
|
+
|
|
616
|
+
const maxScore = results[0].Score; // Results are already sorted by score desc
|
|
617
|
+
if (maxScore <= 0) return results;
|
|
618
|
+
|
|
619
|
+
// Scale so the top result maps to ~0.95 and others proportionally
|
|
620
|
+
const scaleFactor = 0.95 / maxScore;
|
|
621
|
+
for (const r of results) {
|
|
622
|
+
r.Score = Math.min(0.99, r.Score * scaleFactor);
|
|
623
|
+
}
|
|
624
|
+
return results;
|
|
625
|
+
}
|
|
626
|
+
|
|
444
627
|
/** Build Pinecone metadata filter from input */
|
|
445
628
|
private buildPineconeFilter(filters?: SearchFiltersInput): object | undefined {
|
|
446
629
|
if (!filters) return undefined;
|
|
@@ -475,6 +658,9 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
475
658
|
const entityIcon = (meta['EntityIcon'] as string) || undefined;
|
|
476
659
|
const updatedAt = meta['__mj_UpdatedAt'] ? new Date(meta['__mj_UpdatedAt'] as string) : new Date();
|
|
477
660
|
|
|
661
|
+
// Extract tags from vector metadata if stored during indexing
|
|
662
|
+
const metaTags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
663
|
+
|
|
478
664
|
return {
|
|
479
665
|
ID: match.id,
|
|
480
666
|
EntityName: entityName,
|
|
@@ -484,19 +670,47 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
484
670
|
Snippet: snippet,
|
|
485
671
|
Score: match.score ?? 0,
|
|
486
672
|
ScoreBreakdown: { Vector: match.score ?? 0 },
|
|
487
|
-
Tags:
|
|
673
|
+
Tags: metaTags,
|
|
488
674
|
EntityIcon: entityIcon,
|
|
489
675
|
RecordName: title,
|
|
490
676
|
MatchedAt: updatedAt,
|
|
677
|
+
RawMetadata: JSON.stringify(meta),
|
|
491
678
|
};
|
|
492
679
|
});
|
|
493
680
|
}
|
|
494
681
|
|
|
495
|
-
/**
|
|
682
|
+
/**
|
|
683
|
+
* Extract the best display title from vector metadata using entity field metadata.
|
|
684
|
+
* Combines all IsNameField fields in Sequence order (e.g., FirstName + LastName → "Sarah Chen").
|
|
685
|
+
* Falls back to heuristic field name matching when entity metadata isn't available.
|
|
686
|
+
*/
|
|
496
687
|
private extractDisplayTitle(meta: Record<string, unknown>, fallbackEntity: string): string {
|
|
497
|
-
//
|
|
498
|
-
const
|
|
499
|
-
|
|
688
|
+
// 1. Use entity metadata to find IsNameField fields and combine them
|
|
689
|
+
const entityName = meta['Entity'] as string | undefined;
|
|
690
|
+
if (entityName) {
|
|
691
|
+
const md = new Metadata();
|
|
692
|
+
const entityInfo = md.Entities.find(e => e.Name === entityName);
|
|
693
|
+
if (entityInfo) {
|
|
694
|
+
const nameFields = entityInfo.Fields
|
|
695
|
+
.filter(f => f.IsNameField)
|
|
696
|
+
.sort((a, b) => (a.Sequence ?? 9999) - (b.Sequence ?? 9999));
|
|
697
|
+
if (nameFields.length > 0) {
|
|
698
|
+
const parts = nameFields
|
|
699
|
+
.map(f => meta[f.Name])
|
|
700
|
+
.filter(v => v != null && String(v).trim() !== '')
|
|
701
|
+
.map(v => String(v));
|
|
702
|
+
if (parts.length > 0) return parts.join(' ');
|
|
703
|
+
}
|
|
704
|
+
// Single NameField fallback
|
|
705
|
+
if (entityInfo.NameField && meta[entityInfo.NameField.Name]) {
|
|
706
|
+
return String(meta[entityInfo.NameField.Name]);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// 2. Heuristic fallbacks for common field names
|
|
712
|
+
const heuristicFields = ['Name', 'Title', 'Subject', 'Label', 'DisplayName'];
|
|
713
|
+
for (const field of heuristicFields) {
|
|
500
714
|
if (meta[field] && typeof meta[field] === 'string') {
|
|
501
715
|
return meta[field] as string;
|
|
502
716
|
}
|
|
@@ -544,6 +758,49 @@ export class SearchKnowledgeResolver extends ResolverBase {
|
|
|
544
758
|
return Array.from(seen.values()).sort((a, b) => b.Score - a.Score);
|
|
545
759
|
}
|
|
546
760
|
|
|
761
|
+
/**
|
|
762
|
+
* Remove Content Item results that originated from Entity-type content sources.
|
|
763
|
+
* These are redundant because the underlying entity records are already vectorized
|
|
764
|
+
* and searchable directly — showing both creates duplicate results.
|
|
765
|
+
*/
|
|
766
|
+
private async excludeEntitySourcedContentItems(
|
|
767
|
+
results: SearchKnowledgeResultItem[],
|
|
768
|
+
contextUser: UserInfo
|
|
769
|
+
): Promise<SearchKnowledgeResultItem[]> {
|
|
770
|
+
const contentItemResults = results.filter(r => r.EntityName === 'MJ: Content Items');
|
|
771
|
+
if (contentItemResults.length === 0) return results;
|
|
772
|
+
|
|
773
|
+
// Use cached engine data — no RunView needed
|
|
774
|
+
await KnowledgeHubMetadataEngine.Instance.Config(false, contextUser);
|
|
775
|
+
const engine = KnowledgeHubMetadataEngine.Instance;
|
|
776
|
+
|
|
777
|
+
// Find source type ID for "Entity"
|
|
778
|
+
const entitySourceType = engine.ContentSourceTypes.find(st => st.Name === 'Entity');
|
|
779
|
+
if (!entitySourceType) return results;
|
|
780
|
+
|
|
781
|
+
// Collect all ContentSource IDs that are entity-type
|
|
782
|
+
const entitySourceIDs = new Set(
|
|
783
|
+
engine.ContentSources
|
|
784
|
+
.filter(cs => UUIDsEqual(cs.ContentSourceTypeID, entitySourceType.ID))
|
|
785
|
+
.map(cs => cs.ID.toLowerCase())
|
|
786
|
+
);
|
|
787
|
+
if (entitySourceIDs.size === 0) return results;
|
|
788
|
+
|
|
789
|
+
// Filter using ContentSourceID from vector metadata — no DB lookup needed
|
|
790
|
+
return results.filter(r => {
|
|
791
|
+
if (r.EntityName !== 'MJ: Content Items') return true;
|
|
792
|
+
if (!r.RawMetadata) return true;
|
|
793
|
+
try {
|
|
794
|
+
const meta = JSON.parse(r.RawMetadata) as Record<string, string>;
|
|
795
|
+
const sourceID = meta.ContentSourceID;
|
|
796
|
+
if (!sourceID) return true;
|
|
797
|
+
return !entitySourceIDs.has(sourceID.toLowerCase());
|
|
798
|
+
} catch {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
547
804
|
/** Enrich results with entity icons and record names */
|
|
548
805
|
private async enrichResults(results: SearchKnowledgeResultItem[], contextUser: UserInfo): Promise<void> {
|
|
549
806
|
const md = new Metadata();
|