Skip to content

Commit fa9056c

Browse files
author
Soroush Saffari
authored
Adopts Retrievable Explanation (#250)
## What is the goal of this PR? As of Grakn 1.6.0 and since the changes made in the explanation retrieval with Client Node.js, explaining the inferred concepts were not possible against Core 1.6.0. This PR adopts these changes and brings back the explanation feature of Workbase. ## What are the changes implemented in this PR? ### Attaching `queryPattern` to nodes Within the `EXPLAIN_CONCEPT` action in order to know if the retrieved explanation is a joint explanation or one resolved from a rule, we check the variables of the explanation answers against the original `queryPattern`. For this reason, we need to have access to the node's `queryPattern` and so we attach it to the node when building the node object. ### Retrieving explanation In the legacy implementation of explanation retrieval, we could safely assume that when `queryPattern` is an empty string, the explanation is of type Joint and so we need to retrieve explanations one level deeper. This is no longer the case, and therefore we rely on the `isJointExplanation` variable which checks the variables of the retrieved explanation answers against the original `queryPattern`.
1 parent ee38068 commit fa9056c

File tree

8 files changed

+11793
-83
lines changed

8 files changed

+11793
-83
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.6
1+
1.2.7

dependencies/graknlabs/dependencies.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ def graknlabs_build_tools():
2222
git_repository(
2323
name = "graknlabs_build_tools",
2424
remote = "https://github.com/graknlabs/build-tools",
25-
commit = "d8e266644cc5c1d44dc17515069b1a6b70c98c44", # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_build_tools
25+
commit = "2917a6d9348d67b036d762f521e6a8680101ea51", # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_build_tools
2626
)
2727

2828
def graknlabs_grakn_core():
2929
git_repository(
3030
name = "graknlabs_grakn_core",
3131
remote = "https://github.com/graknlabs/grakn",
32-
commit = "2939a8464096c73b42ec0702647a246f357f226b", # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_grakn_core
32+
commit = "86f8a19a9bdac3093f5d131e0fe07667b047ca82", # sync-marker: do not remove this comment, this is used for sync-dependencies by @graknlabs_grakn_core
3333
)
3434

3535
def graknlabs_client_nodejs():

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,4 @@
165165
"unit": "node ./node_modules/jest/bin/jest.js ./test/unit"
166166
},
167167
"version": "1.2.4"
168-
}
168+
}

src/renderer/components/Visualiser/store/actions.js

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import CDB from '../../shared/CanvasDataBuilder';
2929
import { reopenTransaction } from '../../shared/SharedUtils';
3030

3131

32+
const collect = (array, current) => array.concat(current);
33+
3234
export default {
3335
[INITIALISE_VISUALISER]({ state, commit, dispatch }, { container, visFacade }) {
3436
addResetGraphListener(dispatch, CANVAS_RESET);
@@ -129,7 +131,7 @@ export default {
129131
PATH: 'compute path',
130132
};
131133

132-
// eslint-disable-next-line no-prototype-builtins
134+
// eslint-disable-next-line no-prototype-builtins
133135
const queryType = (result[0].hasOwnProperty('map') ? queryTypes.GET : queryTypes.PATH);
134136

135137
let nodes = [];
@@ -217,41 +219,49 @@ export default {
217219
}
218220
},
219221
async [EXPLAIN_CONCEPT]({ state, getters, commit, rootState }) {
220-
const explanation = getters.selectedNode.explanation;
221-
const graknTx = global.graknTx[rootState.activeTab];
222+
try {
223+
const graknTx = global.graknTx[rootState.activeTab];
222224

223-
let queries;
225+
const node = getters.selectedNode;
226+
const queryPattern = node.queryPattern;
227+
const queryPatternVariales = Array.from(new Set(queryPattern.match(/\$[^\s|)|;|,]*/g))).map(x => x.substring(1));
224228

225-
// If the explanation is formed from a conjuction inside a rule, go one step deeper to access the actual explanation
226-
if (!explanation.queryPattern().length) {
227-
queries = explanation.answers().map(answer => answer.explanation().answers().map(answer => mapAnswerToExplanationQuery(answer))).flatMap(x => x);
228-
} else {
229-
queries = explanation.answers().map(answer => mapAnswerToExplanationQuery(answer));
230-
}
229+
const explanationAnswers = (await node.explanation()).getAnswers();
231230

232-
try {
233-
/* eslint-disable no-await-in-loop */
234-
for (const query of queries) { // eslint-disable-line
235-
commit('loadingQuery', true);
236-
const result = await (await graknTx.query(query)).collect();
237-
if (result.length > 0) {
238-
const data = await CDB.buildInstances(result);
239-
240-
const rpData = await CDB.buildRPInstances(result, data, false, graknTx);
241-
data.nodes.push(...rpData.nodes);
242-
data.edges.push(...rpData.edges);
243-
244-
state.visFacade.addToCanvas(data);
245-
commit('updateCanvasData');
246-
const nodesWithAttributes = await computeAttributes(data.nodes, graknTx);
247-
248-
state.visFacade.updateNode(nodesWithAttributes);
249-
const styledEdges = data.edges.map(edge => ({ ...edge, label: edge.hiddenLabel, ...state.visStyle.computeExplanationEdgeStyle() }));
250-
state.visFacade.updateEdge(styledEdges);
251-
commit('loadingQuery', false);
252-
} else {
253-
commit('setGlobalErrorMsg', 'The transaction has been refreshed since the loading of this node and, as a result, the explaination is incomplete.');
231+
const explanationPromises = [];
232+
const explanationResult = [];
233+
234+
explanationAnswers.forEach((answer) => {
235+
const answerVariabes = Array.from(answer.map().keys());
236+
237+
const isJointExplanation = answerVariabes.every(variable => queryPatternVariales.includes(variable));
238+
if (answer.hasExplanation() && isJointExplanation) {
239+
explanationPromises.push(answer.explanation());
240+
} else if (!isJointExplanation) {
241+
explanationResult.push(answer);
254242
}
243+
});
244+
245+
(await Promise.all(explanationPromises)).map(expl => expl.getAnswers()).reduce(collect, []).forEach((expl) => {
246+
explanationResult.push(expl);
247+
});
248+
249+
if (explanationResult.length > 0) {
250+
const data = await CDB.buildInstances(explanationResult);
251+
const rpData = await CDB.buildRPInstances(explanationResult, data, false, graknTx);
252+
data.nodes.push(...rpData.nodes);
253+
data.edges.push(...rpData.edges);
254+
255+
state.visFacade.addToCanvas(data);
256+
commit('updateCanvasData');
257+
const nodesWithAttributes = await computeAttributes(data.nodes, graknTx);
258+
259+
state.visFacade.updateNode(nodesWithAttributes);
260+
const styledEdges = data.edges.map(edge => ({ ...edge, label: edge.hiddenLabel, ...state.visStyle.computeExplanationEdgeStyle() }));
261+
state.visFacade.updateEdge(styledEdges);
262+
commit('loadingQuery', false);
263+
} else {
264+
commit('setGlobalErrorMsg', 'The transaction has been refreshed since the loading of this node and, as a result, the explaination is incomplete.');
255265
}
256266
} catch (e) {
257267
await reopenTransaction(rootState, commit);

src/renderer/components/shared/CanvasDataBuilder.js

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,19 @@ const getEdge = (from, to, edgeType, label) => {
9999
* @param {String} graqlVar
100100
* @param {ConceptMap[]} explanation
101101
*/
102-
const buildCommonInstanceNode = async (instance, graqlVar, explanation) => {
102+
const buildCommonInstanceNode = async (instance, graqlVar, explanation, queryPattern) => {
103103
const node = {};
104104
node.id = instance.id;
105105
node.baseType = instance.baseType;
106106
node.var = graqlVar;
107-
node.explanation = explanation();
108107
node.attrOffset = 0;
109108
node.type = await getConceptLabel(instance);
110109
node.isInferred = await instance.isInferred();
111110
node.attributes = instance.attributes;
111+
if (node.isInferred) {
112+
node.explanation = explanation;
113+
node.queryPattern = queryPattern();
114+
}
112115

113116
return node;
114117
};
@@ -119,8 +122,8 @@ const buildCommonInstanceNode = async (instance, graqlVar, explanation) => {
119122
* @param {String} graqlVar
120123
* @param {ConceptMap[]} explanation
121124
*/
122-
const getInstanceNode = async (instance, graqlVar, explanation) => {
123-
const node = await buildCommonInstanceNode(instance, graqlVar, explanation);
125+
const getInstanceNode = async (instance, graqlVar, explanation, queryPattern) => {
126+
const node = await buildCommonInstanceNode(instance, graqlVar, explanation, queryPattern);
124127
switch (instance.baseType) {
125128
case ENTITY_INSTANCE: {
126129
node.label = `${node.type}: ${node.id}`;
@@ -213,13 +216,15 @@ const getInstanceEdges = async (instance, existingNodeIds) => {
213216
*/
214217
const buildInstances = async (answers) => {
215218
let data = answers.map((answerGroup) => {
216-
const explanation = answerGroup.explanation;
219+
const { explanation, queryPattern } = answerGroup;
217220
return Array.from(answerGroup.map().entries()).map(([graqlVar, concept]) => ({
218221
graqlVar,
219222
concept,
220223
explanation,
224+
queryPattern,
221225
}));
222226
}).reduce(collect, []);
227+
223228
const shouldVisualiseVals = await Promise.all(data.map(item => item.concept.isThing() && shouldVisualiseInstance(item.concept)));
224229
data = data.map((item, index) => {
225230
item.shouldVisualise = shouldVisualiseVals[index];
@@ -228,7 +233,7 @@ const buildInstances = async (answers) => {
228233

229234
data = deduplicateConcepts(data);
230235

231-
const nodes = await Promise.all(data.filter(item => item.shouldVisualise).map(item => getInstanceNode(item.concept, item.graqlVar, item.explanation)));
236+
const nodes = await Promise.all(data.filter(item => item.shouldVisualise).map(item => getInstanceNode(item.concept, item.graqlVar, item.explanation, item.queryPattern)));
232237
const nodeIds = nodes.map(node => node.id);
233238
const edges = (await Promise.all(data.filter(item => item.shouldVisualise).map(item => getInstanceEdges(item.concept, nodeIds)))).reduce(collect, []);
234239

@@ -408,12 +413,12 @@ const buildTypes = async (answers) => {
408413
* @param {*} graqlVar
409414
* @param {*} explanation
410415
*/
411-
const getNeighbourNode = async (concept, graqlVar, explanation) => {
416+
const getNeighbourNode = async (concept, graqlVar, explanation, queryPattern) => {
412417
let node;
413418
if (concept.isType()) {
414419
node = getTypeNode(concept, graqlVar);
415420
} else if (concept.isThing()) {
416-
node = getInstanceNode(concept, graqlVar, explanation);
421+
node = getInstanceNode(concept, graqlVar, explanation, queryPattern);
417422
}
418423
return node;
419424
};
@@ -468,11 +473,12 @@ const getNeighbourEdges = async (neighbourConcept, targetNode, graknTx) => {
468473
*/
469474
const buildNeighbours = async (targetNode, answers, graknTx) => {
470475
let data = answers.map((answerGroup) => {
471-
const explanation = answerGroup.explanation;
476+
const { explanation, queryPattern } = answerGroup;
472477
return Array.from(answerGroup.map().entries()).map(([graqlVar, concept]) => ({
473478
graqlVar,
474479
concept,
475480
explanation,
481+
queryPattern,
476482
}));
477483
}).reduce(collect, []);
478484

@@ -485,11 +491,8 @@ const buildNeighbours = async (targetNode, answers, graknTx) => {
485491

486492
data = deduplicateConcepts(data);
487493

488-
const nodesPromises = Promise.all(data.filter(item => item.shouldVisualise).map(item => getNeighbourNode(item.concept, item.graqlVar, item.explanation)));
489-
const edgesPromises = Promise.all(data.filter(item => item.shouldVisualise).map(item => getNeighbourEdges(item.concept, targetNode, graknTx)));
490-
491-
const nodes = await nodesPromises;
492-
const edges = (await edgesPromises).reduce(collect, []);
494+
const nodes = await Promise.all(data.filter(item => item.shouldVisualise).map(item => getNeighbourNode(item.concept, item.graqlVar, item.explanation, item.queryPattern)));
495+
const edges = (await Promise.all(data.filter(item => item.shouldVisualise).map(item => getNeighbourEdges(item.concept, targetNode, graknTx)))).reduce(collect, []);
493496

494497
return { nodes, edges };
495498
};
@@ -535,7 +538,7 @@ const buildRPInstances = async (answers, currentData, shouldLimit, graknTx) => {
535538
const rp = roleplayers[l];
536539
if (rp.isThing() && await shouldVisualiseInstance(rp)) {
537540
edges.push(getEdge(instance, rp, edgeTypes.instance.RELATES, edgeLabel));
538-
nodes.push(await getInstanceNode(rp, graqlVar, answer.explanation));
541+
nodes.push(await getInstanceNode(rp, graqlVar, answer.explanation, answer.queryPattern));
539542
}
540543
}
541544
}

test/helpers/mockedConcepts.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const getMockedGraknTx = (answers, extraProps = {}) => ({
66
...extraProps,
77
});
88

9-
const getMockedExplanation = answers => ({ answers: () => answers });
9+
const getMockedExplanation = answers => Promise.resolve({ getAnswers: () => answers });
1010

1111
const getMockedAnswer = (concepts, explanation) => {
1212
const answer = {};
@@ -15,6 +15,7 @@ const getMockedAnswer = (concepts, explanation) => {
1515
concepts.forEach((concept, index) => { map.set(index, concept); });
1616
answer.map = () => map;
1717
answer.explanation = () => explanation;
18+
answer.queryPattern = () => '';
1819

1920
return answer;
2021
};

test/unit/components/shared/CanvasDataBuilder.test.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ const expectCommonPropsOnInstanceNode = (node) => {
2424
expect(node).toHaveProperty('id');
2525
expect(node).toHaveProperty('baseType');
2626
expect(node).toHaveProperty('var');
27-
expect(node).toHaveProperty('explanation');
2827
expect(node).toHaveProperty('attrOffset');
2928
expect(node).toHaveProperty('type');
3029
expect(node).toHaveProperty('isInferred');
30+
if (node.isInferred) {
31+
expect(node).toHaveProperty('explanation');
32+
expect(node).toHaveProperty('queryPattern');
33+
}
3134
expect(node).toHaveProperty('attributes');
3235
};
3336

@@ -99,13 +102,15 @@ describe('building instances', () => {
99102
});
100103

101104
test('when graql answer contains an explanation', async () => {
102-
const explConcept = mockedAttributeInstance;
105+
const entityInstance = { ...mockedEntityInstance };
106+
entityInstance.isInferred = () => Promise.resolve(true);
107+
const explConcept = { ...mockedAttributeInstance };
103108
const explAnswer = getMockedAnswer([explConcept], null);
104109
const explanation = getMockedExplanation([explAnswer]);
105-
const answers = [getMockedAnswer([mockedEntityInstance], explanation)];
110+
const answers = [getMockedAnswer([entityInstance], explanation)];
106111
const { nodes } = await CDB.buildInstances(answers);
107-
108-
expect(nodes[0].explanation.answers()[0].map().get(0)).toEqual(explConcept);
112+
const explAnswers = (await nodes[0].explanation()).getAnswers();
113+
expect(explAnswers[0].map().get(0)).toEqual(explConcept);
109114
});
110115

111116
test('when graql answer contains an implicit instance', async () => {

0 commit comments

Comments
 (0)