Skip to content

Commit

Permalink
optimize map marker fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
CollinBeczak committed Feb 25, 2025
1 parent e0aeb47 commit 52f06e2
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 24 deletions.
6 changes: 3 additions & 3 deletions app/org/maproulette/framework/controller/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,13 @@ class TaskController @Inject() (
includeTags: Boolean
): Action[AnyContent] = Action.async { implicit request =>
this.sessionManager.userAwareRequest { implicit user =>
SearchParameters.withSearch { p =>
val params = p.copy(location = Some(SearchLocation(left, bottom, right, top)))
SearchParameters.withSearch { params =>
val result = this.taskClusterService.getTaskMarkerDataInBoundingBox(
User.userOrMocked(user),
params,
limit,
excludeLocked
excludeLocked,
Some(SearchLocation(left, bottom, right, top))
)

val resultJson = this.insertExtraTaskJSON(result, includeGeometries, includeTags)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
package org.maproulette.framework.repository

import java.sql.Connection

import anorm.~
import anorm._
import anorm.SqlParser.{get, int, str}

import javax.inject.{Inject, Singleton}
import org.maproulette.session.SearchParameters
import org.maproulette.framework.psql.{Query, Order, Paging}
import org.maproulette.session.{SearchLocation, SearchParameters}
import org.maproulette.framework.psql.{Order, Paging, Query}
import org.maproulette.framework.model.{ClusteredPoint, Point, TaskCluster}
import play.api.db.Database
import play.api.libs.json._

import org.maproulette.models.dal.ChallengeDAL

@Singleton
Expand Down Expand Up @@ -165,20 +164,80 @@ class TaskClusterRepository @Inject() (override val db: Database, challengeDAL:
}

/**
* Querys task markers in a bounding box
* Queries for task marker data using a CTE-based approach for better performance.
* First filters tasks to a smaller set before joining with other tables.
* Applies spatial filtering in the main query for optimal index usage.
*
* @param query Query to execute
* @param limit Maximum number of results to return
* @param c An available connection
* @return The list of Tasks found within the bounding box
* @param query The query containing all filter parameters
* @param limit Maximum number of results to return
* @return List of ClusteredPoint objects
*/
def queryTaskMarkerDataInBoundingBox(
query: Query,
location: Option[SearchLocation],
limit: Int
): List[ClusteredPoint] = {
this.withMRTransaction { implicit c =>
val finalQuery = query.copy(finalClause = s"LIMIT $limit").build(selectTaskMarkersSQL)
finalQuery.as(this.pointParser.*)
// Extract the location filter if provided
val locationClause = location match {
case Some(loc) =>
s"tasks.location && ST_MakeEnvelope(${loc.left}, ${loc.bottom}, ${loc.right}, ${loc.top}, 4326)"
case None => "TRUE"
}

// Create a modified query that only selects task IDs
val filteredTasksCTE = s"""
WITH filtered_tasks AS (
SELECT tasks.id
FROM tasks
INNER JOIN challenges c ON c.id = tasks.parent_id
INNER JOIN projects p ON p.id = c.parent_id
LEFT OUTER JOIN task_review ON task_review.task_id = tasks.id
WHERE ${query.filter.sql()}
)
"""

// Main query using the CTE
val mainQuery = s"""
SELECT tasks.id,
tasks.name,
tasks.parent_id,
c.name,
tasks.instruction,
tasks.status,
tasks.mapped_on,
tasks.completed_time_spent,
tasks.completed_by,
tasks.bundle_id,
tasks.is_bundle_primary,
tasks.cooperative_work_json::TEXT as cooperative_work,
task_review.review_status,
task_review.review_requested_by,
task_review.reviewed_by,
task_review.reviewed_at,
task_review.review_started_at,
task_review.meta_review_status,
task_review.meta_reviewed_by,
task_review.meta_reviewed_at,
task_review.additional_reviewers,
ST_AsGeoJSON(tasks.location) AS location,
priority,
CASE
WHEN task_review.review_started_at IS NULL THEN 0
ELSE EXTRACT(epoch FROM (task_review.reviewed_at - task_review.review_started_at))
END AS reviewDuration
FROM filtered_tasks
INNER JOIN tasks ON tasks.id = filtered_tasks.id
INNER JOIN challenges c ON c.id = tasks.parent_id
INNER JOIN projects p ON p.id = c.parent_id
LEFT OUTER JOIN task_review ON task_review.task_id = tasks.id
WHERE ${locationClause}
LIMIT ${limit}
"""

val finalSQL = filteredTasksCTE + mainQuery

SQL(finalSQL).as(this.pointParser.*)
}
}

Expand Down
48 changes: 38 additions & 10 deletions app/org/maproulette/framework/service/TaskClusterService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
package org.maproulette.framework.service

import javax.inject.{Inject, Singleton}

import org.maproulette.Config
import org.maproulette.exception.InvalidException
import org.maproulette.framework.model._
import org.maproulette.framework.psql._
import org.maproulette.framework.psql.filter._
import org.maproulette.framework.repository.TaskClusterRepository
import org.maproulette.framework.mixins.{SearchParametersMixin, TaskFilterMixin}
import org.maproulette.session.SearchParameters
import org.maproulette.session.{SearchLocation, SearchParameters}

/**
* Service layer for TaskCluster
Expand Down Expand Up @@ -82,21 +81,50 @@ class TaskClusterService @Inject() (repository: TaskClusterRepository)
}

/**
* This function will retrieve all the task marker data in a given bounded area. You can use various search
* parameters to limit the tasks retrieved in the bounding box area.
* This function will retrieve task marker data in a given bounded area with optimized performance.
* Uses filtering techniques to limit data retrieved and improve query execution time.
*
* @param params The search parameters from the cookie or the query string parameters.
* @param ignoreLocked Whether to include locked tasks (by other users) or not
* @return The list of Tasks found within the bounding box
* @param user The user making the request
* @param params The search parameters including bounding box information
* @param limit Maximum number of points to return
* @param ignoreLocked Whether to include locked tasks (by other users) or not
* @return The list of ClusteredPoints found within the bounding box
*/
def getTaskMarkerDataInBoundingBox(
user: User,
params: SearchParameters,
limit: Int,
ignoreLocked: Boolean = false
ignoreLocked: Boolean = false,
location: Option[SearchLocation] = None
): List[ClusteredPoint] = {
val query = buildQueryForBoundingBox(user, params, ignoreLocked)
this.repository.queryTaskMarkerDataInBoundingBox(query, limit)
// Create a pre-filtered query for basic task conditions
val baseQuery = this.filterOutDeletedParents(this.filterOnSearchParameters(params)(false))

// Apply locking filters
val lockedFilteredQuery = this.filterOutLocked(user, baseQuery, ignoreLocked)

// Add exclusion filter if needed
val finalQuery = params.taskParams.excludeTaskIds match {
case Some(excludedIds) if excludedIds.nonEmpty =>
lockedFilteredQuery.addFilterGroup(
FilterGroup(
List(
BaseParameter(
Task.FIELD_ID,
excludedIds.mkString(","),
Operator.IN,
negate = true,
useValueDirectly = true,
table = Some("tasks")
)
)
)
)
case _ => lockedFilteredQuery
}

// Execute the optimized query
this.repository.queryTaskMarkerDataInBoundingBox(finalQuery, location, limit)
}

/**
Expand Down

0 comments on commit 52f06e2

Please sign in to comment.