Spark UDF optimization for Graph Database (Neo4j) inserts - scala

This is first issue i am posting so apologies if i miss some info and mediocre formatting. I can update if required.
I will try to add as many details as possible. I have a not so optimized Spark Job which converts RDBMS data to graph nodes and relations in Neo4j.
To do this. Here is the steps i follow:
create a denormalized dataframe 'data' with spark sql and joins.
Foreach row in 'data' run a graphInsert function which does the following:
a. read contents of the row b. formulate a neo4j cypher query (We use Merge command so that we have have only one City e.g. Chicago created in Neo4j when Chicago will be present in multiple lines in RDBMS table) c. connect to neo4j d. execute the query e. disconnect from neo4j
Here is the list of problems i am facing.
Inserts are slow.
I know Merge query is slower than create but is there another way to do this instead of connecting and disconnecting for every record? This was my first draft code and maybe i am struggling how i will use one connection to insert from multiple threads on different spark worker nodes. Hence connecting and disconnecting for every record.
The job is not scalable. It only runs fine with 1 core. As soon as i run the job with 2 spark cores i suddenly get 2 cities with same name, even when i am running merge queries. e.g. There are 2 Chicago cities which violates the use of Merge. I am assuming that Merge functions something like "Create if not exist".
I dont know if my implementation is wrong in neo4j part or spark. If anyone can direct me to any documentation which helps me implement this on a better scale it will be helpful as i have a big spark cluster which i need to utilize at full potential for this job.
If you are interested to look at code instead of algorithm. Here is graphInsert implementation in scala:
class GraphInsert extends Serializable{
var case_attributes = new Array[String](4)
var city_attributes = new Array[String](2)
var location_attributes = new Array[String](20)
var incident_attributes = new Array[String](20)
val prop = new Properties()
prop.load(getClass().getResourceAsStream("/GraphInsertConnection.properties"))
// properties Neo4j
val url_neo4j = prop.getProperty("url_neo4j")
val neo4j_user = prop.getProperty("neo4j_user")
val neo4j_password = prop.getProperty("neo4j_password")
def graphInsert(data : Row){
val query = "MERGE (d:CITY {name:city_attributes(0)})\n" +"MERGE (a:CASE { " + case_attributes(0) + ":'" +data(11) + "'," +case_attributes(1) + ":'" +data(13) + "'," +case_attributes(2) + ":'" +data(14) +"'}) \n" +"MERGE (b:INCIDENT { " + incident_attributes(0) + ":" +data(0) + "," +incident_attributes(1) + ":" +data(2) + "," +incident_attributes(2) + ":'" +data(3) + "'," +incident_attributes(3) + ":'" +data(8)+ "'," +incident_attributes(4) + ":" +data(5) + "," +incident_attributes(5) + ":'" +data(4) + "'," +incident_attributes(6) + ":'" +data(6) + "'," +incident_attributes(7) + ":'" +data(1) + "'," +incident_attributes(8) + ":" +data(7)+"}) \n" +"MERGE (c:LOCATION { " + location_attributes(0) + ":" +data(9) + "," +location_attributes(1) + ":" +data(10) + "," +location_attributes(2) + ":'" +data(19) + "'," +location_attributes(3) + ":'" +data(20)+ "'," +location_attributes(4) + ":" +data(18) + "," +location_attributes(5) + ":" +data(21) + "," +location_attributes(6) + ":'" +data(17) + "'," +location_attributes(7) + ":" +data(22) + "," +location_attributes(8) + ":" +data(23)+"}) \n" +"MERGE (a) - [r1:"+relation_case_incident+"]->(b)-[r2:"+relation_incident_location+"]->(c)-[r3:belongs_to]->(d);"
println(query)
try{
var con = DriverManager.getConnection(url_neo4j, neo4j_user, neo4j_password)
var stmt = con.createStatement()
var rs = stmt.executeQuery(query)
con.close()
}catch{
case ex: SQLException =>{
println(ex.getMessage)
}
}
}
def operations(sqlContext: SQLContext){
....
#Get 'data' before this step
city_attributes = entity_metadata.filter(entity_metadata("source_name") === "tb_city").map(x =>x.getString(5)).collect()
case_attributes = entity_metadata.filter(entity_metadata("source_name") === "tb_case_number").map(x =>x.getString(5)).collect()
location_attributes = entity_metadata.filter(entity_metadata("source_name") === "tb_location").map(x =>x.getString(5)).collect()
incident_attributes= entity_metadata.filter(entity_metadata("source_name") === "tb_incident").map(x =>x.getString(5)).collect()
data.foreach(graphInsert)
}
object GraphObject {
def main(args: Array[String]) {
val conf = new SparkConf()
.setAppName("GraphNeo4j")
.setMaster("xyz")
.set("spark.cores.max","2")
.set("spark.executor.memory","10g")
Logger.getLogger("org").setLevel(Level.ERROR)
Logger.getLogger("akka").setLevel(Level.ERROR)
val sc = new SparkContext(conf)
val sqlContext = new SQLContext(sc)
val graph = new GraphInsert()
graph.operations(sqlContext)
}
}

Whatever you write inside the closure i.e it needs to be executed on Worker gets distributed.
You can read more about it here : http://spark.apache.org/docs/latest/programming-guide.html#understanding-closures-a-nameclosureslinka
And as you increase the number of cores, I think it must not effect the application because if you do not specify it ! then it takes the greedy approach ! I hope this document helps .

I am done improving the process but nothing could make it as fast as LOAD command in Cypher.
Hope this helps someone though:
use foreachPartition instead of foreach gives significant gain while doing such process. Also adding periodic commit using cypher.

Related

Unable to write parquet data on local using spark by appending timestamp at the end

I am trying to append the timestamp/date string at the end of the destination path but it fails. If I remove, then there is no error. I tried below things:
val formatDate = new SimpleDateFormat("yyyy-mm-dd hh:ss")
val newDate = formatDate.format(Calendar.getInstance().getTime())
val finalPath = jobInfo.jobId + "_" + jobInfo.jobRunId + "_" + newDate
df.write.mode(SaveMode.Overwrite).parquet(destPath + "\\" + cTableName + "\\" + finalPath.trim())
Error:
java.io.IOException: Mkdirs failed to create file:/C:/tmp/sparkIF/employees/1000_12_2020-31-18 08:11/_temporary/0/_temporary/attempt_20200318203112_0002_m_000000_2
As I can see it is trying to create temporary directories. I am not sure why it is doing so. Since, I am using overwrite mode existing directories shouldn't be a problem
Hadoop do not support semicolon in the directory path
Below Code should work
val finalPath = jobInfo.jobId + "_" + jobInfo.jobRunId + "_" + newDate.replaceAll(":","_")
I think problem in your example in space symbol in path. Better way to save data depends on date using directory date partitioning. Look for example:
val cal = Calendar.getInstance()
val datePartitions = Seq(
s"year=${cal.get(Calendar.YEAR)}",
s"month=${"%02d".format(cal.get(Calendar.MONTH))}",
s"day=${"%02d".format(cal.get(Calendar.DAY_OF_MONTH))}",
s"hour=${"%02d".format(cal.get(Calendar.HOUR))}",
s"minute=${"%02d".format(cal.get(Calendar.MINUTE))}"
).mkString(File.separator)
val finalPath = s"${jobId}_$jobRunId${File.separator}$datePartitions"
println(s"$destPath${File.separator}$cTableName${File.separator}$finalPath")
// it prints your prefix and year=2020\month=02\day=19\hour=10\minute=03 suffix

Performance issue with spark Dataframe, each iteration takes longer

I´m using Spark 2.2.1 with Scala 2.11.12 version as a language to generate a recursive algorithm. First, I tried an implementation using RDD but the time when I used a lot of data was too much. I have made a new version using DataFrames but with very little data it takes too much, taking less data in each iteration than in the previous iteration.
I have tried to cache variables in different ways (types of persistence included), to use checkpoints in different moments, using the repartition method with different values ​​and in different functions, and nothing works.
The code starts looking for the minimum distance between the points that make up the matrix (matrix is a DataFrame):
println("Finding minimum:")
val minDistRes = matrix.select(min("dist")).first().getFloat(0)
val clusterRes = matrix.where($"dist" === minDistRes)
println(s"New minimum:")
clusterRes.show(1)
Then, save the coordenates to the points for later calculations:
val point1 = clusterRes.first().getInt(0)
val point2 = clusterRes.first().getInt(1)
After, made several filters to use them in the new points generated in the next iteration (the creation of a broadcast variable is necessary to be able to access this data in a later map):
matrix = matrix.where("!(idW1 == " + point1 +" and idW2 ==" + point2 + " )").cache()
val dfPoints1 = matrix.where("idW1 == " + point1 + " or idW2 == " + point1).cache()
val dfPoints2 = matrix.where("idW1 == " + point2 + " or idW2 == " + point2).cache()
val dfPoints2Broadcast = spark.sparkContext.broadcast(dfPoints2)
val dfUnionPoints = dfPoints1.union(dfPoints2).cache()
val matrixSub = matrix.except(dfUnionPoints).cache()
Continued with the calculation of the new points and I return the matrix that will be used recursively by the algorithm:
val newPoints = dfPoints1.map{
r => val distAux = dfPoints2Broadcast.value.where("idW1 == " + r.getInt(0) +
" or idW1 == " + r.getInt(1) + " or idW2 == " + r.getInt(0) + " or idW2 == " +
r.getInt(1)).first().getFloat(2)
(newIndex.toInt, filterDF(r.getInt(0),r.getInt(1), point1, point2), math.min(r.getFloat(2), distAux))
}.asInstanceOf[Dataset[Row]]
matrix = matrixSub.union(newPoints)
Finalize each iteration caching the matrix variable and realized a checkpoint every so often:
matrix.cache()
if (a % 5 == 0)
matrix.checkpoint()

Load S3 files in parallel Spark

I am successfully loading files into Spark, from S3, through the following code. It's working, however I am noticing that there is a delay between 1 file and another, and they are loaded sequentially. I would like to improve this by loading in parallel.
// Load files that were loaded into firehose on this day
var s3Files = spark.sqlContext.read.schema(schema).json("s3n://" + job.awsaccessKey + ":" + job.awssecretKey + "#" + job.bucketName + "/" + job.awss3RawFileExpression + "/" + year + "/" + monthCheck + "/" + dayCheck + "/*/").rdd
// Apply the schema to the RDD, here we will have duplicates
val usersDataFrame = spark.createDataFrame(s3Files, schema)
usersDataFrame.createOrReplaceTempView("results")
// Clean and use partition by the keys to eliminate duplicates and get latest record
var results = spark.sql(buildCleaningQuery(job, "results"))
results.createOrReplaceTempView("filteredResults")
val records = spark.sql("select count(*) from filteredResults")
I have also tried loading through the textFile() method, however then I am having problems converting RDD[String] to RDD[Row] because afterwards I would need to move on to use Spark SQL. I am using it in the following manner;
var s3Files = sparkContext.textFile("s3n://" + job.awsaccessKey + ":" + job.awssecretKey + "#" + job.bucketName + "/" + job.awss3RawFileExpression + "/" + year + "/" + monthCheck + "/" + dayCheck + "/*/").toJavaRDD()
What is the ideal manner to load JSON files (Multiple files around 50MB each) into Spark? I would like to validate the properties against a schema, so I would later on be able to Spark SQL queries to clean data.
What's going on is that DataFrame is being converted into RDD and then into DataFrame again, which then loses the partitioning information.
var s3Files = spark
.sqlContext
.read.schema(schema)
.json(...)
.createOrRepla‌​ceTempView("results"‌​)
should be sufficient, and the partitioning information should still be present, allowing json files to be loaded concurrently.

Improve performance on native sql query

I'm trying to build a location-based application in Java EE, which returns providers from a certain radius given coordinates as input. As I use JPA, I couldn't find a solution to querying the database with a named query as it doesn't support functions like Sin, Cos etc. I found a nice read here, sql-haversine, where I could use a native query instead. The query executes fast (33 ms) on the database (Postgres), but very slow when executed from the application (> 800 ms).
Is there a way to speed things up, from the application end, or do you have to use something like a stored procedure in the database.
My method looks like this:
#SuppressWarnings("unchecked")
public List<Company> findCompaniesByProximity(Coordinate coordinate, int distance) {
double latitude = coordinate.getLatitude();
double longitude = coordinate.getLongitude();
String sql = "SELECT * FROM ("
+ "SELECT *, c.distance_unit * DEGREES(ACOS(COS(RADIANS(c.lat)) * COS(RADIANS(companies.latitude)) * "
+ "COS(RADIANS(c.lng) - RADIANS(companies.longitude)) + SIN(RADIANS(c.lat)) * SIN(RADIANS(companies.latitude)))"
+ ") AS distance "
+ "FROM Companies "
+ "JOIN (SELECT ?1 AS lat, ?2 AS lng, ?3 AS search_radius, 111.045 AS distance_unit) "
+ "AS c ON 1=1 "
+ "WHERE companies.latitude "
+ "BETWEEN c.lat - (c.search_radius / c.distance_unit) "
+ "AND c.lat + (c.search_radius / c.distance_unit) "
+ "AND companies.longitude "
+ "BETWEEN c.lng - (c.search_radius / (c.distance_unit * COS(RADIANS(c.lat)))) "
+ "AND c.lng + (c.search_radius / (c.distance_unit * COS(RADIANS(c.lat))))"
+ ") AS proximity "
+ "WHERE distance <= search_radius "
+ "ORDER BY distance "
+ "LIMIT 25";
List<Company> companies = null;
try {
Query query = em.createNativeQuery(sql, Company.class);
query.setParameter(1, latitude);
query.setParameter(2, longitude);
query.setParameter(3, distance / 1000.0);
companies = (List<Company>) query.getResultList();
} catch (IllegalArgumentException e) {
System.err.println("Argument is invalid " + e);
} catch (PersistenceException e) {
System.err.println("PersistenceException: " + e.getMessage());
}
return companies;
}
The method is called from a singleton EJB, and I'm using Payara server with Postgres and EclipseLink. Everything is called locally so I thought the connection to database would be much faster. Iv'e also tried the postgres earthdistance extension, but it was even slower ( > 1800 ms). I've quite new to programming and especially Java EE so I might have done something wrong along the way :)
Thanks, in advance.

Data Access Layer for Analysis Services w/Dynamic MDX

We have project that uses Analysis Services as it's datasource. To try to avoid having to create 100's of queries because of all the selection options we allow, we create our mdx queries with alot of switches and string concatenation. This is our "Data Access Layer". It is a beast to manage and the smallest mistake: missing spaces, mispellings are easy to miss and even easier to accidently include. Does anyone know of a good resource that can help make this more manageable, like a tutorial, white paper or sample project.
To give you an idea of the case logic I'm talking about and it goes on and on...
if (Time == Day)
{
if (Years == One)
{
return (" MEMBER " + CurrentSalesPercent +
" AS ([Sales % " + YearString + " " + StatusType + "]) ");
}
else //2Y
{
return (" MEMBER " + CurrentSalesPercent +
" AS ([Sales % 2Y " + StatusType + "]) ");
}
}
else if (Time == Week)
{
if (Years == One)
{
return (" MEMBER " + CurrentSalesPercent +
" AS ([Sales WTD % " + YearString + " " + StatusType + "]) ");
}
else //2Y
{
return (" MEMBER " + CurrentSalesPercent +
" AS ([Sales WTD % 2Y " + StatusType + "]) ");
}
...
To be honest, I'm not sure if all the different measures and calculations are correct either. But, that's controlled by another team, so we have a little less influence here.
Thanks!
mkt
Have you looked at the way MS generates MDX? If you have SSRS installed, get "Red gate Reflector" and disassemble C:\Program Files\Microsoft SQL Server\MSRS10.MSSQLSERVER\Reporting Services\ReportServer\bin\MDXQueryGenerator.dll
Apart from that, pre-canned queries that take parameters seems pretty standard :(