From a2b9f5fe34cbab70a6bf0c7ecc6f67a462ecf35f Mon Sep 17 00:00:00 2001 From: rjawesome Date: Mon, 10 Jun 2024 14:48:42 -0700 Subject: [PATCH 1/8] Implement edge constraints for normal queries --- src/graph/knowledge_graph.ts | 3 + src/index.ts | 46 ++++- src/query_edge.ts | 251 ++++++++------------------ src/results_assembly/query_results.ts | 13 +- 4 files changed, 125 insertions(+), 188 deletions(-) diff --git a/src/graph/knowledge_graph.ts b/src/graph/knowledge_graph.ts index 706fe0f8..109dbcd4 100644 --- a/src/graph/knowledge_graph.ts +++ b/src/graph/knowledge_graph.ts @@ -9,6 +9,7 @@ import { TrapiKGNodes, TrapiQualifier, TrapiSource, + TrapiAttributeConstraint, } from '@biothings-explorer/types'; import KGNode from './kg_node'; import KGEdge from './kg_edge'; @@ -162,6 +163,8 @@ export default class KnowledgeGraph { } update(bteGraph: BTEGraphUpdate): void { + this.nodes = {}; + this.edges = {}; Object.keys(bteGraph.nodes).map((node) => { this.nodes[bteGraph.nodes[node].primaryCurie] = this._createNode(bteGraph.nodes[node]); }); diff --git a/src/index.ts b/src/index.ts index 59401b36..741f01b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ export default class TRAPIQueryHandler { auxGraphs: TrapiAuxGraphCollection; finalizedResults: TrapiResult[]; queryGraph: TrapiQueryGraph; + queryGraphHandler: QueryGraph; constructor( options: QueryHandlerOptions = {}, smartAPIPath: string = undefined, @@ -209,6 +210,7 @@ export default class TRAPIQueryHandler { let auxGraphs: { [supportGraphID: string]: TrapiAuxiliaryGraph } = {}; const edgesToRebind = {}; const edgesIDsByAuxGraphID = {}; + const supportGraphsByEdgeID = {}; Object.entries(this.bteGraph.edges).forEach(([edgeID, bteEdge]) => { if (edgeID.includes('expanded')) return; const supportGraph = [edgeID]; @@ -227,6 +229,7 @@ export default class TRAPIQueryHandler { suffix += 1; } const supportGraphID = `support${suffix}-${boundEdgeID}`; + supportGraphsByEdgeID[edgeID] = supportGraphID; auxGraphs[supportGraphID] = { edges: supportGraph, attributes: [] }; if (!edgesIDsByAuxGraphID[supportGraphID]) { edgesIDsByAuxGraphID[supportGraphID] = new Set(); @@ -291,6 +294,32 @@ export default class TRAPIQueryHandler { newBindings.push(binding); boundIDs.add(binding.id); } + } else if (this.queryGraph.edges[qEdgeID].attribute_constraints) { + const oldBoundEdge = this.bteGraph.edges[edgesToRebind[binding.id]]; + const newBoundEdge = `${edgesToRebind[binding.id]}-constrained_by_${qEdgeID}`; + if (!boundIDs.has(newBoundEdge)) { + this.bteGraph.edges[newBoundEdge] = new KGEdge(newBoundEdge, { + predicate: oldBoundEdge.predicate, + subject: oldBoundEdge.subject, + object: oldBoundEdge.object, + }); + this.bteGraph.edges[newBoundEdge].addAdditionalAttributes('biolink:support_graphs', []); + this.bteGraph.edges[newBoundEdge].addAdditionalAttributes('biolink:knowledge_level', 'logical_entailment') + this.bteGraph.edges[newBoundEdge].addAdditionalAttributes('biolink:agent_type', 'automated_agent') + this.bteGraph.edges[newBoundEdge].addSource([ + { + resource_id: this.options.provenanceUsesServiceProvider + ? 'infores:service-provider-trapi' + : 'infores:biothings-explorer', + resource_role: 'primary_knowledge_source', + }, + ]); + boundIDs.add(newBoundEdge); + newBindings.push({...binding, id: newBoundEdge }); + resultBoundEdgesWithAuxGraphs.add(newBoundEdge); + } + (this.bteGraph.edges[newBoundEdge].attributes['biolink:support_graphs'] as Set).add(supportGraphsByEdgeID[binding.id]); + edgesIDsByAuxGraphID[supportGraphsByEdgeID[binding.id]].add(newBoundEdge); } else if (!boundIDs.has(edgesToRebind[binding.id])) { newBindings.push({ id: edgesToRebind[binding.id], attributes: [] }); boundIDs.add(edgesToRebind[binding.id]); @@ -407,6 +436,7 @@ export default class TRAPIQueryHandler { } } } + this.queryGraphHandler = new QueryGraph(queryGraph, this.options.schema); } _initializeResponse(): void { @@ -416,11 +446,10 @@ export default class TRAPIQueryHandler { this.bteGraph.subscribe(this.knowledgeGraph); } - async _processQueryGraph(queryGraph: TrapiQueryGraph): Promise { + async _processQueryGraph(): Promise { try { - const queryGraphHandler = new QueryGraph(queryGraph, this.options.schema); - const queryEdges = await queryGraphHandler.calculateEdges(); - this.logs = [...this.logs, ...queryGraphHandler.logs]; + const queryEdges = await this.queryGraphHandler.calculateEdges(); + this.logs = [...this.logs, ...this.queryGraphHandler.logs]; return queryEdges; } catch (err) { if (err instanceof InvalidQueryGraphError || err instanceof SRINodeNormFailure) { @@ -548,7 +577,7 @@ export default class TRAPIQueryHandler { Object.values(this.queryGraph).forEach((item) => { Object.values(item).forEach((element: any) => { element.constraints?.forEach((constraint: { name: string }) => constraints.add(constraint.name)); - element.attribute_constraints?.forEach((constraint: { name: string }) => constraints.add(constraint.name)); + // element.attribute_constraints?.forEach((constraint: { name: string }) => constraints.add(constraint.name)); // element.qualifier_constraints?.forEach((constraint) => constraints.add(constraint.name)); }); }); @@ -656,7 +685,7 @@ export default class TRAPIQueryHandler { ); } - const queryEdges = await this._processQueryGraph(this.queryGraph); + const queryEdges = await this._processQueryGraph(); // TODO remove this when constraints implemented if (await this._checkContraints()) { return; @@ -691,10 +720,13 @@ export default class TRAPIQueryHandler { // update query graph this.bteGraph.update(manager.getRecords()); + this.bteGraph.notify(); //update query results await this.trapiResultsAssembler.update( manager.getOrganizedRecords(), - !(this.options.smartAPIID || this.options.teamName), + this.queryGraphHandler, + this.knowledgeGraph.kg, + !(this.options.smartAPIID || this.options.teamName) ); this.logs = [...this.logs, ...this.trapiResultsAssembler.logs]; // fix subclassing diff --git a/src/query_edge.ts b/src/query_edge.ts index a9513741..1dcb857f 100644 --- a/src/query_edge.ts +++ b/src/query_edge.ts @@ -6,7 +6,7 @@ import { Record, RecordNode, FrozenRecord } from '@biothings-explorer/api-respon import QNode from './query_node'; import { QNodeInfo } from './query_node'; import { LogEntry, StampedLog } from '@biothings-explorer/utils'; -import { TrapiAttributeConstraint, TrapiQualifierConstraint } from '@biothings-explorer/types'; +import { TrapiAttribute, TrapiAttributeConstraint, TrapiKGEdge, TrapiKGNode, TrapiQualifierConstraint } from '@biothings-explorer/types'; const debug = Debug('bte:biothings-explorer-trapi:QEdge'); @@ -32,6 +32,7 @@ interface QEdgeInfo { executed?: boolean; reverse?: boolean; qualifier_constraints?: TrapiQualifierConstraint[]; + attribute_constraints?: TrapiAttributeConstraint[]; frozen?: boolean; predicates?: string[]; } @@ -51,6 +52,7 @@ export default class QEdge { object: QNode; expanded_predicates: string[]; qualifier_constraints: TrapiQualifierConstraint[]; + constraints: TrapiAttributeConstraint[]; reverse: boolean; executed: boolean; logs: StampedLog[]; @@ -64,6 +66,7 @@ export default class QEdge { this.object = info.frozen === true ? new QNode(info.object as QNodeInfo) : (info.object as QNode); this.expanded_predicates = []; this.qualifier_constraints = info.qualifier_constraints || []; + this.constraints = info.attribute_constraints || []; this.reverse = this.subject?.getCurie?.() === undefined && this.object?.getCurie?.() !== undefined; @@ -351,188 +354,13 @@ export default class QEdge { !this.reverse ? this.subject.updateCuries(combined_curies_2) : this.object.updateCuries(combined_curies_2); } - applyNodeConstraints(): void { - debug(`(6) Applying Node Constraints to ${this.records.length} records.`); - const kept = []; - let save_kept = false; - const sub_constraints = this.subject.constraints; - if (sub_constraints && sub_constraints.length) { - const from = this.reverse ? 'object' : 'subject'; - debug(`Node (subject) constraints: ${JSON.stringify(sub_constraints)}`); - save_kept = true; - for (let i = 0; i < this.records.length; i++) { - const res = this.records[i]; - let keep = true; - // apply constraints - for (let x = 0; x < sub_constraints.length; x++) { - const constraint = sub_constraints[x]; - keep = this.meetsConstraint(constraint, res, from); - } - // pass or not - if (keep) { - kept.push(res); - } - } - } - - const obj_constraints = this.object.constraints; - if (obj_constraints && obj_constraints.length) { - const from = this.reverse ? 'subject' : 'object'; - debug(`Node (object) constraints: ${JSON.stringify(obj_constraints)}`); - save_kept = true; - for (let i = 0; i < this.records.length; i++) { - const res = this.records[i]; - let keep = true; - // apply constraints - for (let x = 0; x < obj_constraints.length; x++) { - const constraint = obj_constraints[x]; - keep = this.meetsConstraint(constraint, res, from); - } - // pass or not - if (keep) { - kept.push(res); - } - } - } - if (save_kept) { - // only override recordss if there was any filtering done. - this.records = kept; - debug(`(6) Reduced to (${this.records.length}) records.`); - } else { - debug(`(6) No constraints. Skipping...`); - } - } - - meetsConstraint(constraint: TrapiAttributeConstraint, record: Record, from: string): boolean { - // list of attribute ids in node - const available_attributes = [...new Set(Object.keys(record[from].attributes))]; - // debug(`ATTRS ${JSON.stringify(record[from].normalizedInfo[0]._leafSemanticType)}` + - // ` ${from} : ${JSON.stringify(available_attributes)}`); - // determine if node even contains right attributes - const filters_found = available_attributes.filter((attr) => attr == constraint.id); - if (!filters_found.length) { - // node doesn't have the attribute needed - return false; - } else { - // match attr by name, parse only attrs of interest - const node_attributes = {}; - filters_found.forEach((filter) => { - node_attributes[filter] = record[from].attributes[filter]; - }); - switch (constraint.operator) { - case '==': - for (const key in node_attributes) { - if (!isNaN(constraint.value as number)) { - if (Array.isArray(node_attributes[key])) { - if ( - node_attributes[key].includes(constraint.value) || - node_attributes[key].includes(constraint.value.toString()) - ) { - return true; - } - } else { - if ( - node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value as string) - ) { - return true; - } - } - } else { - if (Array.isArray(node_attributes[key])) { - if (node_attributes[key].includes(constraint.value)) { - return true; - } - } else { - if ( - node_attributes[key] == constraint.value || - node_attributes[key] == constraint.value.toString() || - node_attributes[key] == parseInt(constraint.value as string) - ) { - return true; - } - } - } - } - return false; - case '>': - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value as string)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) > parseInt(constraint.value as string)) { - return true; - } - } - } - return false; - case '>=': - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) >= parseInt(constraint.value as string)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) >= parseInt(constraint.value as string)) { - return true; - } - } - } - return false; - case '<': - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) > parseInt(constraint.value as string)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) < parseInt(constraint.value as string)) { - return true; - } - } - } - return false; - case '<=': - for (const key in node_attributes) { - if (Array.isArray(node_attributes[key])) { - for (let index = 0; index < node_attributes[key].length; index++) { - const element = node_attributes[key][index]; - if (parseInt(element) <= parseInt(constraint.value as string)) { - return true; - } - } - } else { - if (parseInt(node_attributes[key]) <= parseInt(constraint.value as string)) { - return true; - } - } - } - return false; - default: - debug(`Node operator not handled ${constraint.operator}`); - return false; - } - } - } - + storeRecords(records: Record[]): void { + debug((new Error()).stack) debug(`(6) Storing records...`); // store new records in current edge this.records = records; // will update records if any constraints are found - this.applyNodeConstraints(); debug(`(7) Updating nodes based on edge records...`); this.updateNodesCuries(records); } @@ -577,4 +405,71 @@ export default class QEdge { getReversedPredicate(predicate: string): string { return predicate ? biolink.reverse(predicate) : undefined; } + + meetsConstraints(kgEdge: TrapiKGEdge, kgSub: TrapiKGNode, kgObj: TrapiKGNode): boolean { + // edge constraints + if (this.constraints) { + for (let constraint of this.constraints) { + let meets = this._meetsConstraint(constraint, kgEdge.attributes); + if (constraint.not) meets = !meets; + if (!meets) return false; + } + } + + // node constraints not fully tested yet (may be some weird behavior with subclsasing) + // subject constraints + if (this.subject.constraints) { + for (let constraint of this.subject.constraints) { + let meets = this._meetsConstraint(constraint, kgSub.attributes); + if (constraint.not) meets = !meets; + if (!meets) return false; + } + } + + // object constraints + if (this.object.constraints) { + for (let constraint of this.object.constraints) { + let meets = this._meetsConstraint(constraint, kgObj.attributes); + if (constraint.not) meets = !meets; + if (!meets) return false; + } + } + + return true; + } + + _meetsConstraint(constraint: TrapiAttributeConstraint, attributes?: TrapiAttribute[]): boolean { + const edge_attribute = attributes?.find(x => x.attribute_type_id == constraint.id)?.value as any; + const constraintValue = constraint.value as any; + if (!edge_attribute) { + return false; + } + switch (constraint.operator) { + case '==': + const array1 = utils.toArray(edge_attribute); + const array2 = utils.toArray(constraintValue); + for (let a1 of array1) { + for (let a2 of array2) { + if (a1 == a2) return true; + } + } + return false; + case '===': + if (Array.isArray(edge_attribute) && Array.isArray(constraintValue)) { + if (edge_attribute.length !== constraintValue.length) return false; + for (let i = 0; i < edge_attribute.length; i++) { + if (edge_attribute[i] !== constraintValue[i]) return false; + } + return true; + } + return edge_attribute === constraintValue; + case '>': + return edge_attribute > constraintValue; + case '<': + return edge_attribute < constraintValue; + default: + debug(`Node operator not handled ${constraint.operator}`); + return false; + } + } } diff --git a/src/results_assembly/query_results.ts b/src/results_assembly/query_results.ts index 51dff31c..048baf6a 100644 --- a/src/results_assembly/query_results.ts +++ b/src/results_assembly/query_results.ts @@ -1,5 +1,5 @@ import { LogEntry, StampedLog } from '@biothings-explorer/utils'; -import { TrapiResult } from '@biothings-explorer/types'; +import { TrapiKnowledgeGraph, TrapiResult } from '@biothings-explorer/types'; import Debug from 'debug'; import { zip } from 'lodash'; const debug = Debug('bte:biothings-explorer-trapi:QueryResult'); @@ -7,6 +7,7 @@ import { getScores, calculateScore, ScoreCombos } from './score'; import { Record } from '@biothings-explorer/api-response-transform'; import { enrichTrapiResultsWithPfocrFigures } from './pfocr'; import * as config from '../config'; +import QueryGraph from '../query_graph'; export interface RecordsByQEdgeID { [qEdgeID: string]: { @@ -150,6 +151,8 @@ export default class TrapiResultsAssembler { _getQueryGraphSolutions( recordsByQEdgeID: RecordsByQEdgeID, qEdgeID: string, + queryGraph: QueryGraph, + kg: TrapiKnowledgeGraph, edgeCount: number, queryGraphSolutions: QueryGraphSolutionEdge[][], queryGraphSolution: QueryGraphSolutionEdge[], @@ -189,7 +192,7 @@ export default class TrapiResultsAssembler { records .filter((record) => { - return [getMatchingPrimaryCurie(record), undefined].indexOf(primaryCurieToMatch) > -1; + return [getMatchingPrimaryCurie(record), undefined].indexOf(primaryCurieToMatch) > -1 && queryGraph.edges[qEdgeID].meetsConstraints(kg.edges[record.recordHash], kg.nodes[kg.edges[record.recordHash].subject], kg.nodes[kg.edges[record.recordHash].object]); }) .forEach((record, i) => { // primaryCurie example: 'NCBIGene:1234' @@ -222,6 +225,8 @@ export default class TrapiResultsAssembler { this._getQueryGraphSolutions( recordsByQEdgeID, connectedQEdgeID, + queryGraph, + kg, edgeCount, queryGraphSolutions, queryGraphSolution, @@ -272,7 +277,7 @@ export default class TrapiResultsAssembler { * can safely assume every call to update contains all the records. * */ - async update(recordsByQEdgeID: RecordsByQEdgeID, shouldScore = true): Promise { + async update(recordsByQEdgeID: RecordsByQEdgeID, queryGraph: QueryGraph, kg: TrapiKnowledgeGraph, shouldScore = true): Promise { debug(`Updating query results now!`); let scoreCombos: ScoreCombos; @@ -317,6 +322,8 @@ export default class TrapiResultsAssembler { this._getQueryGraphSolutions( recordsByQEdgeID, initialQEdgeID, + queryGraph, + kg, qEdgeCount, queryGraphSolutions, [], // first queryGraphSolution From b261d5e11ff653c45453a561b12a739f4d86a572 Mon Sep 17 00:00:00 2001 From: rjawesome Date: Tue, 11 Jun 2024 10:34:27 -0700 Subject: [PATCH 2/8] matches edge constraint --- src/query_edge.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/query_edge.ts b/src/query_edge.ts index 1dcb857f..331b893f 100644 --- a/src/query_edge.ts +++ b/src/query_edge.ts @@ -463,6 +463,18 @@ export default class QEdge { return true; } return edge_attribute === constraintValue; + case 'matches': + if (typeof constraintValue === 'string') { + let regexStr = constraintValue; + // make sure regex matches the whole string + if (constraintValue.at(0) !== '^') regexStr = '^' + regexStr; + if (constraintValue.at(constraintValue.length - 1) !== '$') regexStr += '$'; + let regex = new RegExp(regexStr); + for (let attr of utils.toArray(edge_attribute)) { + if (regex.test(attr)) return true; + } + } + return false; case '>': return edge_attribute > constraintValue; case '<': From 7aa6bd642f5f315f1bfe79c074ad0d36ae8cf51b Mon Sep 17 00:00:00 2001 From: rjawesome Date: Tue, 11 Jun 2024 12:28:29 -0700 Subject: [PATCH 3/8] allow node constraints --- src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 741f01b0..4edf5677 100644 --- a/src/index.ts +++ b/src/index.ts @@ -557,6 +557,9 @@ export default class TRAPIQueryHandler { this.logs.push(new LogEntry('WARNING', null, message).getLog()); return; } + if (await this._checkContraints()) { + return; + } const inferredQueryHandler = new InferredQueryHandler( this, this.queryGraph, @@ -577,7 +580,7 @@ export default class TRAPIQueryHandler { Object.values(this.queryGraph).forEach((item) => { Object.values(item).forEach((element: any) => { element.constraints?.forEach((constraint: { name: string }) => constraints.add(constraint.name)); - // element.attribute_constraints?.forEach((constraint: { name: string }) => constraints.add(constraint.name)); + element.attribute_constraints?.forEach((constraint: { name: string }) => constraints.add(constraint.name)); // element.qualifier_constraints?.forEach((constraint) => constraints.add(constraint.name)); }); }); @@ -593,7 +596,7 @@ export default class TRAPIQueryHandler { new LogEntry( 'ERROR', null, - `BTE does not currently support any type of constraint. Your query Terminates.`, + `BTE does not currently support constraints with creative mode. Your query Terminates.`, ).getLog(), ); return true; @@ -686,10 +689,7 @@ export default class TRAPIQueryHandler { } const queryEdges = await this._processQueryGraph(); - // TODO remove this when constraints implemented - if (await this._checkContraints()) { - return; - } + if ((this.options.smartAPIID || this.options.teamName) && Object.values(this.queryGraph.edges).length > 1) { const message = 'smartAPI/team-specific endpoints only support single-edge queries. Your query terminates.'; this.logs.push(new LogEntry('WARNING', null, message).getLog()); From b2d138987d4e8b58aea4feb95c8556a3a47cd42e Mon Sep 17 00:00:00 2001 From: rjawesome Date: Tue, 11 Jun 2024 13:13:36 -0700 Subject: [PATCH 4/8] move edge filtering earlier --- src/edge_manager.ts | 36 ++++++++++++++++++++++++--- src/index.ts | 2 -- src/query_edge.ts | 1 - src/results_assembly/query_results.ts | 10 ++------ 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/edge_manager.ts b/src/edge_manager.ts index 543a6ea5..a5815bfe 100644 --- a/src/edge_manager.ts +++ b/src/edge_manager.ts @@ -14,6 +14,8 @@ import { UnavailableAPITracker } from './types'; import { RecordsByQEdgeID } from './results_assembly/query_results'; import path from 'path'; import { promises as fs } from 'fs'; +import KnowledgeGraph from './graph/knowledge_graph'; +import BTEGraph from './graph/graph'; export default class QueryEdgeManager { private _qEdges: QEdge[]; @@ -211,7 +213,7 @@ export default class QueryEdgeManager { subjectIDs.some((curie) => execSubjectCuries.includes(curie)) || execSubjectCuries.length === 0; const objectMatch = objectIDs.some((curie) => execObjectCuries.includes(curie)) || execObjectCuries.length === 0; - //if both ends match then keep record + // if both ends match then keep record // Don't keep self-edges const selfEdge = [...subjectIDs].some((curie) => objectIDs.includes(curie)); @@ -230,6 +232,32 @@ export default class QueryEdgeManager { return keep; } + _constrainEdgeRecords(qEdge: QEdge, records: Record[]) { + const keep: Record[] = []; + const bte = new BTEGraph(); + const kg = new KnowledgeGraph(); + bte.update(records); + kg.update(bte); + records.forEach(record => { + const edge = kg.kg.edges[record.recordHash]; + const sub = qEdge.reverse ? kg.kg.nodes[edge.object] : kg.kg.nodes[edge.subject]; + const obj = qEdge.reverse ? kg.kg.nodes[edge.subject] : kg.kg.nodes[edge.object]; + if (qEdge.meetsConstraints(edge, sub, obj)) { + keep.push(record); + } + }); + + debug(`'${qEdge.getID()}' dropped (${records.length - keep.length}) records based on edge/node constraints.`); + this.logs.push( + new LogEntry( + 'DEBUG', + null, + `'${qEdge.getID()}' kept (${keep.length}) / dropped (${records.length - keep.length}) records (based on node/edge constraints).`, + ).getLog(), + ); + return keep; + } + collectRecords(): boolean { //go through edges and collect records organized by edge let recordsByQEdgeID: RecordsByQEdgeID = {}; @@ -296,8 +324,10 @@ export default class QueryEdgeManager { updateEdgeRecords(currentQEdge: QEdge): void { //1. filter edge records based on current status - const filteredRecords = this._filterEdgeRecords(currentQEdge); - //2.trigger node update / entity update based on new status + let filteredRecords = this._filterEdgeRecords(currentQEdge); + //2. make sure node/edge constraints are met + filteredRecords = this._constrainEdgeRecords(currentQEdge, filteredRecords); + //3. trigger node update / entity update based on new status currentQEdge.storeRecords(filteredRecords); } diff --git a/src/index.ts b/src/index.ts index 4edf5677..06431863 100644 --- a/src/index.ts +++ b/src/index.ts @@ -724,8 +724,6 @@ export default class TRAPIQueryHandler { //update query results await this.trapiResultsAssembler.update( manager.getOrganizedRecords(), - this.queryGraphHandler, - this.knowledgeGraph.kg, !(this.options.smartAPIID || this.options.teamName) ); this.logs = [...this.logs, ...this.trapiResultsAssembler.logs]; diff --git a/src/query_edge.ts b/src/query_edge.ts index 331b893f..c6416e76 100644 --- a/src/query_edge.ts +++ b/src/query_edge.ts @@ -356,7 +356,6 @@ export default class QEdge { storeRecords(records: Record[]): void { - debug((new Error()).stack) debug(`(6) Storing records...`); // store new records in current edge this.records = records; diff --git a/src/results_assembly/query_results.ts b/src/results_assembly/query_results.ts index 048baf6a..5d1f95f9 100644 --- a/src/results_assembly/query_results.ts +++ b/src/results_assembly/query_results.ts @@ -151,8 +151,6 @@ export default class TrapiResultsAssembler { _getQueryGraphSolutions( recordsByQEdgeID: RecordsByQEdgeID, qEdgeID: string, - queryGraph: QueryGraph, - kg: TrapiKnowledgeGraph, edgeCount: number, queryGraphSolutions: QueryGraphSolutionEdge[][], queryGraphSolution: QueryGraphSolutionEdge[], @@ -192,7 +190,7 @@ export default class TrapiResultsAssembler { records .filter((record) => { - return [getMatchingPrimaryCurie(record), undefined].indexOf(primaryCurieToMatch) > -1 && queryGraph.edges[qEdgeID].meetsConstraints(kg.edges[record.recordHash], kg.nodes[kg.edges[record.recordHash].subject], kg.nodes[kg.edges[record.recordHash].object]); + return [getMatchingPrimaryCurie(record), undefined].indexOf(primaryCurieToMatch) > -1; }) .forEach((record, i) => { // primaryCurie example: 'NCBIGene:1234' @@ -225,8 +223,6 @@ export default class TrapiResultsAssembler { this._getQueryGraphSolutions( recordsByQEdgeID, connectedQEdgeID, - queryGraph, - kg, edgeCount, queryGraphSolutions, queryGraphSolution, @@ -277,7 +273,7 @@ export default class TrapiResultsAssembler { * can safely assume every call to update contains all the records. * */ - async update(recordsByQEdgeID: RecordsByQEdgeID, queryGraph: QueryGraph, kg: TrapiKnowledgeGraph, shouldScore = true): Promise { + async update(recordsByQEdgeID: RecordsByQEdgeID, shouldScore = true): Promise { debug(`Updating query results now!`); let scoreCombos: ScoreCombos; @@ -322,8 +318,6 @@ export default class TrapiResultsAssembler { this._getQueryGraphSolutions( recordsByQEdgeID, initialQEdgeID, - queryGraph, - kg, qEdgeCount, queryGraphSolutions, [], // first queryGraphSolution From c38a5238e0310cb4d4ae99f96847e742d4492019 Mon Sep 17 00:00:00 2001 From: rjawesome Date: Wed, 12 Jun 2024 12:07:26 -0700 Subject: [PATCH 5/8] test for meetsConstraints --- __test__/integration/QueryEdge.test.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/__test__/integration/QueryEdge.test.ts b/__test__/integration/QueryEdge.test.ts index ff2c3195..b5c90081 100644 --- a/__test__/integration/QueryEdge.test.ts +++ b/__test__/integration/QueryEdge.test.ts @@ -1,5 +1,8 @@ import QNode from '../../src/query_node'; import QEdge from '../../src/query_edge'; +import KGNode from '../../src/graph/kg_node'; +import KGEdge from '../../src/graph/kg_edge'; +import KnowledgeGraph from '../../src/graph/knowledge_graph'; describe('Testing QueryEdge Module', () => { const gene_node1 = new QNode({ id: 'n1', categories: ['Gene'], ids: ['NCBIGene:1017'] }); @@ -143,7 +146,7 @@ describe('Testing QueryEdge Module', () => { expect(res).toContain('contributes_to'); expect(res).toContain('causes'); expect(res).toContain('ameliorates'); - expect(res).toContain('treats'); + // expect(res).toContain('treats'); }); test('Predicates not in biolink model should return itself', () => { @@ -198,4 +201,20 @@ describe('Testing QueryEdge Module', () => { // NOTE: recently changed from not.toEqual, because an unfrozen edge *should* equal its original? expect(qEdge1.getHashedEdgeRepresentation()).toEqual(qEdge2.getHashedEdgeRepresentation()); }); + + test('meetsConstraints', () => { + const qEdge = new QEdge({ id: 'e01', subject: type_node, object: disease1_node, predicates: ['biolink:treats'], attribute_constraints: [{ name: 'publications', id: 'biolink:publications', operator: '==', value: 'PMID:9248614', not: false }] }); + const kgNode1 = new KGNode("node1", { label: "node1", semanticType: [], names: [], curies: [], primaryCurie: "node1", qNodeID: "e01"}); + const kgNode2 = new KGNode("node2", { label: "node2", semanticType: [], names: [], curies: [], primaryCurie: "node2", qNodeID: "e01"}); + const kgEdge1 = new KGEdge("edge1", {object: "node1", subject: "node2", predicate: "biolink:treats"}); + const kgEdge2 = new KGEdge("edge2", {object: "node1", subject: "node2", predicate: "biolink:treats"}); + kgEdge1.addPublication("PMID:9248614"); + kgEdge1.addPublication("PMID:1234567"); + kgEdge2.addPublication("PMID:7614243"); + kgEdge2.addPublication("PMID:1234567"); + const graph = new KnowledgeGraph(); + graph.update({ nodes: { node1: kgNode1, node2: kgNode2 }, edges: { edge1: kgEdge1, edge2: kgEdge2 } }); + expect(qEdge.meetsConstraints(graph.edges["edge1"], graph.nodes["node1"], graph.nodes["node2"])).toBeTruthy(); + expect(qEdge.meetsConstraints(graph.edges["edge2"], graph.nodes["node1"], graph.nodes["node2"])).toBeFalsy(); + }) }); From 90a5cf0faf0a858612b760f119b2c7f17542eb40 Mon Sep 17 00:00:00 2001 From: rjawesome Date: Fri, 11 Oct 2024 10:41:02 -0700 Subject: [PATCH 6/8] fix compilation error --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 37f6cb9a..dfff9a5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -337,7 +337,7 @@ export default class TRAPIQueryHandler { newBindings.push({...binding, id: newBoundEdge }); resultBoundEdgesWithAuxGraphs.add(newBoundEdge); } - (this.bteGraph.edges[newBoundEdge].attributes['biolink:support_graphs'] as Set).add(supportGraphsByEdgeID[binding.id]); + (this.bteGraph.edges[newBoundEdge].attributes['biolink:support_graphs'] as Set).add(supportGraphsByEdgeID[binding.id][subId][objId]); edgesIDsByAuxGraphID[supportGraphsByEdgeID[binding.id][subId][objId]].add(newBoundEdge); } else if (!boundIDs.has(edgesToRebind[binding.id]?.[subId]?.[objId])) { From 23ad403672fcf9cd27a9fe315500e9b5aafcfb9a Mon Sep 17 00:00:00 2001 From: rjawesome Date: Wed, 16 Oct 2024 15:46:55 -0700 Subject: [PATCH 7/8] update test --- __test__/unittest/TRAPIQueryHandler.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__test__/unittest/TRAPIQueryHandler.test.ts b/__test__/unittest/TRAPIQueryHandler.test.ts index 7ea16825..8485d061 100644 --- a/__test__/unittest/TRAPIQueryHandler.test.ts +++ b/__test__/unittest/TRAPIQueryHandler.test.ts @@ -35,7 +35,8 @@ describe('test TRAPIQueryHandler methods', () => { edges: {}, }; try { - await handler._processQueryGraph(invalidQueryGraph); + handler.setQueryGraph(invalidQueryGraph); + await handler._processQueryGraph(); } catch (err) { expect(err).toBeInstanceOf(InvalidQueryGraphError); } From 8772de87dd068215160e1434e4df871740778697 Mon Sep 17 00:00:00 2001 From: rjawesome Date: Wed, 16 Oct 2024 15:48:27 -0700 Subject: [PATCH 8/8] fix handling of subclassing + constrsaints --- src/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index dfff9a5c..2f2e2d1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -258,6 +258,7 @@ export default class TRAPIQueryHandler { suffix += 1; } const supportGraphID = `support${suffix}-${boundEdgeID}`; + if (!supportGraphsByEdgeID[edgeID]) supportGraphsByEdgeID[edgeID] = {}; if (!supportGraphsByEdgeID[edgeID][subject]) supportGraphsByEdgeID[edgeID][subject] = {}; supportGraphsByEdgeID[edgeID][subject][object] = supportGraphID; @@ -313,9 +314,14 @@ export default class TRAPIQueryHandler { boundIDs.add(binding.id); } } - else if (this.queryGraph.edges[qEdgeID].attribute_constraints) { + // we only want to include support graphs that meet constraints (in case there is another QEdge using this same KGEdge) + else if ( + this.queryGraph.edges[qEdgeID].attribute_constraints || + this.queryGraph.nodes[this.queryGraph.edges[qEdgeID].subject].constraints || + this.queryGraph.nodes[this.queryGraph.edges[qEdgeID].object].constraints + ) { const oldBoundEdge = this.bteGraph.edges[edgesToRebind[binding.id]?.[subId]?.[objId]]; - const newBoundEdge = `${edgesToRebind[binding.id]}-constrained_by_${qEdgeID}`; + const newBoundEdge = `${edgesToRebind[binding.id][subId][objId]}-constrained_by_${qEdgeID}`; if (!boundIDs.has(newBoundEdge)) { this.bteGraph.edges[newBoundEdge] = new KGEdge(newBoundEdge, { predicate: oldBoundEdge.predicate,