All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.gds.projection.GraphImporter Maven / Gradle / Ivy

/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j 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.
 *
 * This program 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 this program.  If not, see .
 */
package org.neo4j.gds.projection;

import org.jetbrains.annotations.Nullable;
import org.neo4j.gds.ElementProjection;
import org.neo4j.gds.RelationshipType;
import org.neo4j.gds.api.DatabaseInfo;
import org.neo4j.gds.api.DefaultValue;
import org.neo4j.gds.api.compress.AdjacencyCompressor;
import org.neo4j.gds.api.schema.ImmutableMutableGraphSchema;
import org.neo4j.gds.api.schema.MutableGraphSchema;
import org.neo4j.gds.api.schema.MutableRelationshipSchema;
import org.neo4j.gds.api.schema.RelationshipSchema;
import org.neo4j.gds.config.GraphProjectConfig;
import org.neo4j.gds.core.Aggregation;
import org.neo4j.gds.core.loading.Capabilities.WriteMode;
import org.neo4j.gds.core.loading.GraphStoreBuilder;
import org.neo4j.gds.core.loading.GraphStoreCatalog;
import org.neo4j.gds.core.loading.ImmutableNodes;
import org.neo4j.gds.core.loading.ImmutableStaticCapabilities;
import org.neo4j.gds.core.loading.LazyIdMapBuilder;
import org.neo4j.gds.core.loading.RelationshipImportResult;
import org.neo4j.gds.core.loading.construction.GraphFactory;
import org.neo4j.gds.core.loading.construction.ImmutablePropertyConfig;
import org.neo4j.gds.core.loading.construction.NodeLabelToken;
import org.neo4j.gds.core.loading.construction.PropertyValues;
import org.neo4j.gds.core.loading.construction.RelationshipsBuilder;
import org.neo4j.gds.core.utils.ProgressTimer;
import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker;
import org.neo4j.gds.core.utils.progress.tasks.Task;
import org.neo4j.gds.core.utils.progress.tasks.Tasks;
import org.neo4j.gds.utils.StringJoining;

import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toMap;
import static org.neo4j.gds.Orientation.NATURAL;
import static org.neo4j.gds.Orientation.UNDIRECTED;

public final class GraphImporter {

    public static final int NO_TARGET_NODE = -1;

    private final GraphProjectConfig config;
    private final List undirectedRelationshipTypes;
    private final List inverseIndexedRelationshipTypes;
    private final LazyIdMapBuilder idMapBuilder;

    private final WriteMode writeMode;
    private final String query;

    private final ProgressTracker progressTracker;

    private final Map relImporters;
    private final ImmutableMutableGraphSchema.Builder graphSchemaBuilder;

    public static Task graphImporterTask(int taskVolume) {
        return Tasks.task(
            "Graph aggregation",
            Tasks.leaf("Update aggregation", taskVolume),
            Tasks.task("Build graph store", Tasks.leaf("Nodes", 1), Tasks.leaf("Relationships", 1))
        );
    }

    public GraphImporter(
        GraphProjectConfig config,
        List undirectedRelationshipTypes,
        List inverseIndexedRelationshipTypes,
        LazyIdMapBuilder idMapBuilder,
        WriteMode writeMode,
        String query,
        ProgressTracker progressTracker
    ) {
        this.config = config;
        this.undirectedRelationshipTypes = undirectedRelationshipTypes;
        this.inverseIndexedRelationshipTypes = inverseIndexedRelationshipTypes;
        this.idMapBuilder = idMapBuilder;
        this.writeMode = writeMode;
        this.query = query;
        this.progressTracker = progressTracker;
        this.relImporters = new ConcurrentHashMap<>();
        this.graphSchemaBuilder = MutableGraphSchema.builder();

        progressTracker.beginSubTask("Graph aggregation");
        progressTracker.beginSubTask("Update aggregation");
    }

    public void update(
        long sourceNode,
        long targetNode,
        @Nullable PropertyValues sourceNodePropertyValues,
        @Nullable PropertyValues targetNodePropertyValues,
        NodeLabelToken sourceNodeLabels,
        NodeLabelToken targetNodeLabels,
        RelationshipType relationshipType,
        @Nullable PropertyValues relationshipProperties
    ) {

        var intermediateSourceId = loadNode(sourceNode, sourceNodeLabels, sourceNodePropertyValues);

        if (targetNode != NO_TARGET_NODE) {
            RelationshipsBuilder relImporter;
            // we do the check before to avoid having to create a new lambda instance on every call
            if (this.relImporters.containsKey(relationshipType)) {
                relImporter = this.relImporters.get(relationshipType);
            } else {
                relImporter = this.relImporters.computeIfAbsent(
                    relationshipType,
                    type -> newRelImporter(type, relationshipProperties)
                );
            }

            var intermediateTargetId = loadNode(targetNode, targetNodeLabels, targetNodePropertyValues);

            if (relationshipProperties != null) {
                if (relationshipProperties.size() == 1) {
                    relationshipProperties.forEach((key, value) -> {
                        var property = RelationshipPropertyExtractor.extractValue(value, DefaultValue.DOUBLE_DEFAULT_FALLBACK);
                        relImporter.addFromInternal(intermediateSourceId, intermediateTargetId, property);
                    });
                } else {
                    var propertyValues = new double[relationshipProperties.size()];
                    int[] index = {0};
                    relationshipProperties.forEach((key, value) -> {
                        var property = RelationshipPropertyExtractor.extractValue(value, DefaultValue.DOUBLE_DEFAULT_FALLBACK);
                        var i = index[0]++;
                        propertyValues[i] = property;
                    });
                    relImporter.addFromInternal(intermediateSourceId, intermediateTargetId, propertyValues);
                }
            } else {
                relImporter.addFromInternal(intermediateSourceId, intermediateTargetId);
            }
        }

        progressTracker.logProgress();
    }

    public AggregationResult result(
        DatabaseInfo databaseInfo,
        ProgressTimer timer,
        boolean hasSeenArbitraryId
    ) {
        progressTracker.endSubTask("Update aggregation");
        progressTracker.beginSubTask("Build graph store");
        progressTracker.beginSubTask("Nodes");
        var graphName = config.graphName();

        if (GraphStoreCatalog.exists(config.username(), databaseInfo.databaseId(), graphName)) {
            throw new IllegalArgumentException("Graph " + graphName + " already exists");
        }

        this.idMapBuilder.prepareForFlush();

        var writeMode = hasSeenArbitraryId
            ? WriteMode.NONE
            : this.writeMode;

        var graphStoreBuilder = new GraphStoreBuilder()
            .concurrency(this.config.readConcurrency())
            .capabilities(ImmutableStaticCapabilities.of(writeMode))
            .databaseInfo(databaseInfo);

        var valueMapper = buildNodesWithProperties(graphStoreBuilder);
        progressTracker.endSubTask("Nodes");

        progressTracker.beginSubTask("Relationships");
        buildRelationshipsWithProperties(graphStoreBuilder, valueMapper);

        var graphStore = graphStoreBuilder.schema(this.graphSchemaBuilder.build()).build();
        validateRelTypes(graphStore.schema().relationshipSchema());
        progressTracker.endSubTask("Relationships");

        GraphStoreCatalog.set(this.config, graphStore);

        var projectMillis = timer.stop().getDuration();

        progressTracker.endSubTask("Build graph store");
        progressTracker.endSubTask("Graph aggregation");

        return AggregationResultBuilder.builder()
            .graphName(graphName)
            .nodeCount(graphStore.nodeCount())
            .relationshipCount(graphStore.relationshipCount())
            .projectMillis(projectMillis)
            .configuration(
                this.config.asProcedureResultConfigurationField()
                    .entrySet()
                    .stream()
                    .filter(e -> e.getValue() != null)
                    .collect(toMap(Map.Entry::getKey, Map.Entry::getValue))
            )
            .query(this.query)
            .build();
    }

    private void validateRelTypes(RelationshipSchema relationshipSchema) {
        var  unusedUndirectedTypes = notProjectedRelationshipTypes(relationshipSchema, undirectedRelationshipTypes);
        if (!unusedUndirectedTypes.isEmpty()) {
            throw new IllegalArgumentException(String.format(Locale.US,
                "Specified undirectedRelationshipTypes `%s` were not projected in the graph. " + "Projected types are: `%s`.",
                unusedUndirectedTypes,
                StringJoining.join(relationshipSchema.availableTypes().stream().map(RelationshipType::name))
            ));
        }
        var unusedInverseTypes = notProjectedRelationshipTypes(relationshipSchema, inverseIndexedRelationshipTypes);
        if (!unusedInverseTypes.isEmpty()) {
            throw new IllegalArgumentException(String.format(
                Locale.US,
                "Specified inverseIndexedRelationshipTypes `%s` were not projected in the graph. " + "Projected types are: `%s`.",
                unusedInverseTypes,
                StringJoining.join(relationshipSchema.availableTypes().stream().map(RelationshipType::name))
            ));
        }
    }

    private List notProjectedRelationshipTypes(RelationshipSchema schema, List givenRelationshipTypes) {
        if (givenRelationshipTypes.contains(ElementProjection.PROJECT_ALL)) {
            return List.of();
        }

        Set typesInSchema = schema.availableTypes().stream()
            .map(RelationshipType::name)
            .collect(Collectors.toSet());

        return givenRelationshipTypes.stream()
            .filter(type -> !typesInSchema.contains(type))
            .toList();
    }

    private RelationshipsBuilder newRelImporter(RelationshipType relType, @Nullable PropertyValues properties) {
    var orientation = this.undirectedRelationshipTypes.contains(relType.name) || this.undirectedRelationshipTypes
        .contains(
            "*"
        )
        ? UNDIRECTED
        : NATURAL;

    boolean indexInverse = inverseIndexedRelationshipTypes.contains(relType.name) || inverseIndexedRelationshipTypes
        .contains("*");

    var relationshipsBuilderBuilder = GraphFactory.initRelationshipsBuilder()
        .nodes(this.idMapBuilder)
        .relationshipType(relType)
        .orientation(orientation)
        .aggregation(Aggregation.NONE)
        .indexInverse(indexInverse)
        .concurrency(this.config.readConcurrency())
        .usePooledBuilderProvider(true);

    if (properties != null) {
        for (String propertyKey : properties.propertyKeys()) {
            relationshipsBuilderBuilder.addPropertyConfig(
                ImmutablePropertyConfig.builder().propertyKey(propertyKey).build()
            );
        }
    }

    return relationshipsBuilderBuilder.build();
}

/**
 * Adds the given node to the internal nodes builder and returns
 * the intermediate node id which can be used for relationships.
 *
 * @return intermediate node id
 */
private long loadNode(
    long node,
    NodeLabelToken nodeLabels,
    @Nullable PropertyValues nodeProperties
) {
    return nodeProperties == null
        ? this.idMapBuilder.addNode(node, nodeLabels)
        : this.idMapBuilder.addNodeWithProperties(
            node,
            nodeProperties,
            nodeLabels
        );
}

private AdjacencyCompressor.ValueMapper buildNodesWithProperties(GraphStoreBuilder graphStoreBuilder) {
    var idMapAndProperties = this.idMapBuilder.build();

    var idMap = idMapAndProperties.idMap();
    var nodeSchema = idMapAndProperties.schema();

    this.graphSchemaBuilder.nodeSchema(nodeSchema);

    var nodes = ImmutableNodes
        .builder()
        .idMap(idMap)
        .schema(nodeSchema)
        .properties(idMapAndProperties.propertyStore())
        .build();

    graphStoreBuilder.nodes(nodes);

    // Relationships are added using their intermediate node ids.
    // In order to map to the final internal ids, we need to use
    // the mapping function of the wrapped id map.
    return idMap.rootIdMap()::toMappedNodeId;
}

private void buildRelationshipsWithProperties(
    GraphStoreBuilder graphStoreBuilder,
    AdjacencyCompressor.ValueMapper valueMapper
) {
    var relationshipImportResultBuilder = RelationshipImportResult.builder();

    var relationshipSchema = MutableRelationshipSchema.empty();
    this.relImporters.forEach((relationshipType, relImporter) -> {
        var relationships = relImporter.build(
            Optional.of(valueMapper),
            Optional.empty()
        );
        relationshipSchema.set(relationships.relationshipSchemaEntry());
        relationshipImportResultBuilder.putImportResult(relationshipType, relationships);
    });

    graphStoreBuilder.relationshipImportResult(relationshipImportResultBuilder.build());
    this.graphSchemaBuilder.relationshipSchema(relationshipSchema);

    // release all references to the builders
    // we are only be called once and don't support double invocations of `result` building
    this.relImporters.clear();
}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy