ai.grakn.graql.internal.reasoner.query.ReasonerAtomicQuery.java Source code

Java tutorial

Introduction

Here is the source code for ai.grakn.graql.internal.reasoner.query.ReasonerAtomicQuery.java

Source

/*
 * Grakn - A Distributed Semantic Database
 * Copyright (C) 2016  Grakn Labs Limited
 *
 * Grakn is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Grakn is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Grakn. If not, see <http://www.gnu.org/licenses/gpl.txt>.
 */

package ai.grakn.graql.internal.reasoner.query;

import ai.grakn.GraknGraph;
import ai.grakn.concept.Concept;
import ai.grakn.concept.RelationType;
import ai.grakn.exception.GraqlQueryException;
import ai.grakn.concept.Type;
import ai.grakn.graql.Graql;
import ai.grakn.graql.Var;
import ai.grakn.graql.admin.Answer;
import ai.grakn.graql.admin.AnswerExplanation;
import ai.grakn.graql.admin.Atomic;
import ai.grakn.graql.admin.Conjunction;
import ai.grakn.graql.admin.ReasonerQuery;
import ai.grakn.graql.admin.Unifier;
import ai.grakn.graql.admin.VarPatternAdmin;
import ai.grakn.graql.internal.query.QueryAnswer;
import ai.grakn.graql.internal.reasoner.UnifierImpl;
import ai.grakn.graql.internal.reasoner.atom.Atom;
import ai.grakn.graql.internal.reasoner.atom.AtomicFactory;
import ai.grakn.graql.internal.reasoner.atom.binary.Relation;
import ai.grakn.graql.internal.reasoner.atom.binary.TypeAtom;
import ai.grakn.graql.internal.reasoner.cache.Cache;
import ai.grakn.graql.internal.reasoner.cache.LazyQueryCache;
import ai.grakn.graql.internal.reasoner.cache.QueryCache;
import ai.grakn.graql.internal.reasoner.explanation.LookupExplanation;
import ai.grakn.graql.internal.reasoner.explanation.RuleExplanation;
import ai.grakn.graql.internal.reasoner.iterator.ReasonerQueryIterator;
import ai.grakn.graql.internal.reasoner.rule.InferenceRule;
import ai.grakn.graql.internal.reasoner.rule.RuleTuple;
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;
import javafx.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static ai.grakn.graql.internal.reasoner.query.QueryAnswerStream.entityTypeFilter;
import static ai.grakn.graql.internal.reasoner.query.QueryAnswerStream.knownFilterWithInverse;
import static ai.grakn.graql.internal.reasoner.utils.ReasonerUtils.getListPermutations;
import static ai.grakn.graql.internal.reasoner.utils.ReasonerUtils.getUnifiersFromPermutations;

/**
 *
 * <p>
 * Base reasoner atomic query. An atomic query is a query constrained to having at most one rule-resolvable atom
 * together with its accompanying constraints (predicates and types).
 * </p>
 *
 * @author Kasper Piskorski
 *
 */
public class ReasonerAtomicQuery extends ReasonerQueryImpl {

    private Atom atom;
    private static final Logger LOG = LoggerFactory.getLogger(ReasonerAtomicQuery.class);

    ReasonerAtomicQuery(Conjunction<VarPatternAdmin> pattern, GraknGraph graph) {
        super(pattern, graph);
        atom = selectAtoms().stream().findFirst().orElse(null);
    }

    ReasonerAtomicQuery(ReasonerQueryImpl query) {
        super(query);
        atom = selectAtoms().stream().findFirst().orElse(null);
    }

    ReasonerAtomicQuery(Atom at) {
        super(at);
        atom = selectAtoms().stream().findFirst().orElse(null);
    }

    @Override
    public ReasonerQuery copy() {
        return new ReasonerAtomicQuery(this);
    }

    @Override
    public boolean equals(Object obj) {
        return !(obj == null || this.getClass() != obj.getClass()) && super.equals(obj);
    }

    @Override
    public int hashCode() {
        return super.hashCode() + 37;
    }

    @Override
    public boolean isAtomic() {
        return true;
    }

    /**
     * @return the atom constituting this atomic query
     */
    public Atom getAtom() {
        return atom;
    }

    @Override
    public boolean addAtomic(Atomic at) {
        if (super.addAtomic(at)) {
            if (atom == null && at.isSelectable())
                atom = (Atom) at;
            return true;
        } else
            return false;
    }

    @Override
    public boolean removeAtomic(Atomic at) {
        if (super.removeAtomic(at)) {
            if (at.equals(atom))
                atom = null;
            return true;
        } else
            return false;
    }

    @Override
    public Set<Atom> selectAtoms() {
        Set<Atom> selectedAtoms = super.selectAtoms();
        if (selectedAtoms.size() != 1) {
            throw GraqlQueryException.nonAtomicQuery(this);
        }
        return selectedAtoms;
    }

    @Override
    public Unifier getUnifier(ReasonerQuery p) {
        if (p == this)
            return new UnifierImpl();
        ReasonerAtomicQuery parent = (ReasonerAtomicQuery) p;
        Unifier unifier = getAtom().getUnifier(parent.getAtom());
        //get type unifiers
        Set<Atom> unified = new HashSet<>();
        getAtom().getTypeConstraints().forEach(type -> {
            Set<Atom> toUnify = Sets.difference(parent.getEquivalentAtoms(type), unified);
            Atom equiv = toUnify.stream().findFirst().orElse(null);
            //only apply if unambiguous
            if (equiv != null && toUnify.size() == 1) {
                unifier.merge(type.getUnifier(equiv));
                unified.add(equiv);
            }
        });
        return unifier;
    }

    /**
     * resolve the query by performing either a db or memory lookup, depending on which is more appropriate
     * @param cache container of already performed query resolutions
     */
    public Stream<Answer> lookup(Cache<ReasonerAtomicQuery, ?> cache) {
        boolean queryVisited = cache.contains(this);
        return queryVisited ? cache.getAnswerStream(this) : DBlookup(cache);
    }

    /**
     * check whether specific answer to this query exists in cache/db
     * @param cache qieru cache
     * @param sub specific answer
     * @return found answer if any, otherwise empty answer
     */
    Answer lookupAnswer(QueryCache<ReasonerAtomicQuery> cache, Answer sub) {
        boolean queryVisited = cache.contains(this);
        if (queryVisited) {
            Answer answer = cache.getAnswer(this, sub);
            if (!answer.isEmpty())
                return answer;
        }

        List<Answer> match = new ReasonerAtomicQuery(this).addSubstitution(sub).getMatchQuery().execute();
        return match.isEmpty() ? new QueryAnswer() : match.iterator().next();
    }

    Pair<Stream<Answer>, Unifier> lookupWithUnifier(Cache<ReasonerAtomicQuery, ?> cache) {
        boolean queryVisited = cache.contains(this);
        return queryVisited ? cache.getAnswerStreamWithUnifier(this) : new Pair<>(DBlookup(), new UnifierImpl());
    }

    private Stream<Answer> DBlookup() {
        return getMatchQuery().admin().stream().map(QueryAnswer::new)
                .map(a -> a.explain(new LookupExplanation(this)));
    }

    /**
     * resolve the query by performing a db lookup with subsequent cache update
     */
    private Stream<Answer> DBlookup(Cache<ReasonerAtomicQuery, ?> cache) {
        return cache.record(this, DBlookup());
    }

    /**
     * execute insert on the query and return inserted answers
     */
    private Stream<Answer> insert() {
        return Graql.insert(getPattern().getVars()).withGraph(graph()).stream();
    }

    public Stream<Answer> materialise(Answer answer) {
        ReasonerAtomicQuery queryToMaterialise = new ReasonerAtomicQuery(this);
        queryToMaterialise.addSubstitution(answer);
        return queryToMaterialise.insert().map(ans -> ans.setExplanation(answer.getExplanation()));
    }

    private Set<Unifier> getPermutationUnifiers(Atom headAtom) {
        if (!(atom.isRelation() && headAtom.isRelation()))
            return Collections.singleton(new UnifierImpl());

        //if atom is match all atom, add type from rule head and find unmapped roles
        Relation relAtom = atom.getType() == null
                ? ((Relation) AtomicFactory.create(atom, atom.getParentQuery())).addType(headAtom.getType())
                : (Relation) atom;
        List<Var> permuteVars = new ArrayList<>(relAtom.getUnmappedRolePlayers());
        if (!(atom.isRelation() && headAtom.isRelation()) || permuteVars.isEmpty())
            return Collections.singleton(new UnifierImpl());

        List<List<Var>> varPermutations = getListPermutations(new ArrayList<>(permuteVars)).stream()
                .filter(l -> !l.isEmpty()).collect(Collectors.toList());
        return getUnifiersFromPermutations(permuteVars, varPermutations);
    }

    private Stream<Answer> getIdPredicateAnswerStream(Stream<Answer> stream) {
        Answer idPredicateAnswer = getSubstitution();
        return stream.map(answer -> {
            AnswerExplanation exp = answer.getExplanation();
            return answer.merge(idPredicateAnswer).explain(exp);
        });
    }

    private Stream<Answer> getFilteredAnswerStream(Stream<Answer> answers) {
        Set<Var> vars = getVarNames();
        Set<TypeAtom> mappedTypeConstraints = atom.getMappedTypeConstraints();
        return getIdPredicateAnswerStream(answers).filter(a -> entityTypeFilter(a, mappedTypeConstraints))
                .map(a -> a.filterVars(vars));
    }

    /**
     * attempt query resolution via application of a specific rule
     * @param rule rule to apply to resolve the query
     * @param subGoals set of visited subqueries
     * @param cache collection of performed query resolutions
     * @param materialise materialisation flag
     * @return answers from rule resolution
     */
    private Stream<Answer> resolveViaRule(InferenceRule rule, Unifier ruleUnifier, Unifier permutationUnifier,
            Set<ReasonerAtomicQuery> subGoals, Cache<ReasonerAtomicQuery, ?> cache,
            Cache<ReasonerAtomicQuery, ?> dCache, boolean materialise, boolean explanation,
            boolean differentialJoin) {
        Atom atom = this.getAtom();

        LOG.trace("Applying rule to: " + this + rule);
        LOG.trace("t: " + ruleUnifier);
        LOG.trace("tp: " + permutationUnifier);

        ReasonerQueryImpl ruleBody = rule.getBody();
        ReasonerAtomicQuery ruleHead = rule.getHead();
        Set<Var> varsToRetain = rule.hasDisconnectedHead() ? ruleBody.getVarNames() : ruleHead.getVarNames();

        subGoals.add(this);
        Stream<Answer> answers = ruleBody
                .computeJoin(subGoals, cache, dCache, materialise, explanation, differentialJoin)
                .map(a -> a.filterVars(varsToRetain)).distinct()
                .map(ans -> ans.explain(new RuleExplanation(this, rule)));

        if (materialise || rule.requiresMaterialisation(atom)) {
            if (!cache.contains(ruleHead))
                dCache.record(ruleHead, ruleHead.lookup(cache));
            //filter known to make sure no duplicates are inserted (put behaviour)
            Map<Pair<Var, Concept>, Set<Answer>> known = cache.getInverseAnswerMap(ruleHead);
            Map<Pair<Var, Concept>, Set<Answer>> dknown = dCache.getInverseAnswerMap(ruleHead);

            answers = answers.filter(a -> knownFilterWithInverse(a, known))
                    .filter(a -> knownFilterWithInverse(a, dknown)).flatMap(ruleHead::materialise);

            answers = dCache.record(ruleHead, answers);
        }

        //unify answers
        boolean isHeadEquivalent = this.isEquivalent(ruleHead);
        Set<Var> queryVars = this.getVarNames().size() < ruleHead.getVarNames().size() ? ruleUnifier.keySet()
                : ruleHead.getVarNames();
        answers = answers.map(a -> a.filterVars(queryVars)).map(a -> a.unify(ruleUnifier))
                .map(a -> a.unify(permutationUnifier)).filter(a -> !a.isEmpty());

        //if query not exactly equal to the rule head, do some conversion
        return isHeadEquivalent ? dCache.record(this, answers)
                : dCache.record(this, getFilteredAnswerStream(answers));
    }

    /**
     * resolves the query by performing lookups and rule resolution and returns a stream of new answers
     * @param subGoals visited subGoals (recursive queries)
     * @param cache global query cache
     * @param dCache differential query cache
     * @param materialise whether inferred information should be materialised
     * @return stream of differential answers
     */
    Stream<Answer> answerStream(Set<ReasonerAtomicQuery> subGoals, Cache<ReasonerAtomicQuery, ?> cache,
            Cache<ReasonerAtomicQuery, ?> dCache, boolean materialise, boolean explanation,
            boolean differentialJoin) {
        boolean queryAdmissible = !subGoals.contains(this);

        LOG.trace("AQ: " + this);

        Stream<Answer> answerStream = cache.contains(this) ? Stream.empty() : dCache.record(this, lookup(cache));
        if (queryAdmissible) {

            Iterator<RuleTuple> ruleIterator = getRuleIterator();
            while (ruleIterator.hasNext()) {
                RuleTuple ruleContext = ruleIterator.next();
                InferenceRule rule = ruleContext.getRule();
                Unifier u = ruleContext.getRuleUnifier();
                Unifier pu = ruleContext.getPermutationUnifier();

                Answer sub = this.getSubstitution().unify(u.inverse());
                rule.getHead().addSubstitution(sub);
                rule.getBody().addSubstitution(sub);

                Stream<Answer> localStream = resolveViaRule(rule, u, pu, subGoals, cache, dCache, materialise,
                        explanation, differentialJoin);
                answerStream = Stream.concat(answerStream, localStream);
            }
        }

        return dCache.record(this, answerStream);
    }

    @Override
    public Stream<Answer> resolve(boolean materialise, boolean explanation,
            LazyQueryCache<ReasonerAtomicQuery> cache, LazyQueryCache<ReasonerAtomicQuery> dCache) {
        if (!this.getAtom().isRuleResolvable()) {
            return this.getMatchQuery().admin().stream().map(QueryAnswer::new);
        } else {
            return new QueryAnswerIterator(materialise, explanation, cache, dCache).hasStream();
        }
    }

    @Override
    public Iterator<Answer> iterator(Answer sub, Set<ReasonerAtomicQuery> subGoals,
            QueryCache<ReasonerAtomicQuery> cache) {
        Iterator<ReasonerAtomicQueryIterator> qIterator = getQueryStream(sub)
                .map(q -> new ReasonerAtomicQueryIterator(q, sub, subGoals, cache)).iterator();
        return Iterators.concat(qIterator);
    }

    /**
     * @return stream of atomic query obtained by inserting all inferred possible types (if ambiguous)
     */
    private Stream<ReasonerAtomicQuery> getQueryStream(Answer sub) {
        Atom atom = getAtom();
        if (!atom.isRelation() || atom.getType() != null)
            return Stream.of(this);
        else {
            List<RelationType> relationTypes = ((Relation) atom).inferPossibleRelationTypes(sub);
            LOG.trace("AQ: " + this + ": inferred rel types for: "
                    + relationTypes.stream().map(Type::getLabel).collect(Collectors.toList()));
            return relationTypes.stream()
                    .map(type -> ((Relation) AtomicFactory.create(atom, atom.getParentQuery())).addType(type))
                    .map(ReasonerAtomicQuery::new);
        }
    }

    /**
     * @return iterator of all rules applicable to this atomic query including permuted cases when the role types are blank
     */
    Iterator<RuleTuple> getRuleIterator() {
        return getAtom().getApplicableRules().stream().flatMap(r -> {
            r.rewriteToUserDefined(getAtom());
            Unifier ruleUnifier = r.getUnifier(getAtom());
            Unifier ruleUnifierInv = ruleUnifier.inverse();
            return getPermutationUnifiers(r.getHead().getAtom())
                    .stream().map(
                            permutationUnifier -> new RuleTuple(
                                    new InferenceRule(r).propagateConstraints(getAtom(),
                                            permutationUnifier.combine(ruleUnifierInv)),
                                    ruleUnifier, permutationUnifier));
        }).sorted(Comparator.comparing(rt -> -rt.getRule().resolutionPriority())).iterator();
    }

    /**
     *
     * <p>
     * Iterator for query answers maintaining the iterative behaviour of QSQ scheme.
     * </p>
     *
     * @author Kasper Piskorski
     *
     */
    private class QueryAnswerIterator extends ReasonerQueryIterator {

        private int iter = 0;
        private long answers = 0;
        private final boolean materialise;
        private final boolean explanation;
        private final Set<ReasonerAtomicQuery> subGoals = new HashSet<>();

        private final LazyQueryCache<ReasonerAtomicQuery> cache;
        private final LazyQueryCache<ReasonerAtomicQuery> dCache;
        private Iterator<Answer> answerIterator;

        QueryAnswerIterator(boolean materialise, boolean explanation, LazyQueryCache<ReasonerAtomicQuery> cache,
                LazyQueryCache<ReasonerAtomicQuery> dCache) {
            this.materialise = materialise;
            this.explanation = explanation;
            this.cache = cache;
            this.dCache = dCache;
            this.answerIterator = query().answerStream(subGoals, cache, dCache, materialise, explanation, iter != 0)
                    .iterator();
        }

        private ReasonerAtomicQuery query() {
            return ReasonerAtomicQuery.this;
        }

        /**
         * @return stream constructed out of the answer iterator
         */
        @Override
        public Stream<Answer> hasStream() {
            Iterable<Answer> iterable = () -> this;
            return StreamSupport.stream(iterable.spliterator(), false).distinct().peek(ans -> answers++);
        }

        private void computeNext() {
            iter++;
            subGoals.clear();
            answerIterator = query().answerStream(subGoals, cache, dCache, materialise, explanation, iter != 0)
                    .iterator();
        }

        /**
         * check whether answers available, if answers not fully computed compute more answers
         * @return true if answers available
         */
        @Override
        public boolean hasNext() {
            if (answerIterator.hasNext())
                return true;
            //iter finished
            else {
                updateCache();
                long dAns = differentialAnswerSize();
                if (dAns != 0 || iter == 0) {
                    LOG.debug("Atom: " + query().getAtom() + " iter: " + iter + " answers: " + answers + " dAns = "
                            + dAns);
                    computeNext();
                    return answerIterator.hasNext();
                } else
                    return false;
            }
        }

        private void updateCache() {
            dCache.remove(cache);
            cache.add(dCache);
            cache.reload();
        }

        /**
         * @return single answer to the query
         */
        @Override
        public Answer next() {
            return answerIterator.next();
        }

        private long differentialAnswerSize() {
            return dCache.answerSize(subGoals);
        }
    }
}