diff --git a/backend/migrations/20240322224230_create_entities_cache.sql b/backend/migrations/20240322224230_create_entities_cache.sql index d22203b..a4814bb 100644 --- a/backend/migrations/20240322224230_create_entities_cache.sql +++ b/backend/migrations/20240322224230_create_entities_cache.sql @@ -1,7 +1,29 @@ -- Create the materialized view with a deterministic ID CREATE MATERIALIZED VIEW entities_caches AS +-- Get the indexed fields for each family +WITH families_indexed_fields AS ( + SELECT + f.id AS family_id, + ( + SELECT jsonb_object_agg(field->>'key', field->>'field_type') + FROM jsonb_array_elements(f.entity_form->'fields') AS field + WHERE + (field->>'indexed')::boolean IS TRUE + AND + (field->>'field_type')::text IN ('EnumSingleOption', 'EnumMultiOption') + ) AS indexed_enums, + ( + SELECT jsonb_object_agg(field->>'key', field->>'field_type') + FROM jsonb_array_elements(f.entity_form->'fields') AS field + WHERE + (field->>'indexed')::boolean IS TRUE + AND + (field->>'field_type')::text IN ('SingleLineText', 'MultiLineText') + ) AS indexed_strings + FROM families f +), -- For each location of each parent, get a row with the parent and its location flattened -WITH transitive_locations AS ( +transitive_locations AS ( SELECT ee.child_id, e.id AS parent_id, @@ -27,7 +49,30 @@ direct_locations AS ( e.hidden, location.value as location, location.ordinality AS location_index, - array_remove(array_agg(DISTINCT et.tag_id), NULL) AS tags_ids + array_remove(array_agg(DISTINCT et.tag_id), NULL) AS tags_ids, + COALESCE( + jsonb_object_agg( + key, + CASE + WHEN jsonb_typeof(transformed_fields.value) = 'array' THEN transformed_fields.value + ELSE + CASE + WHEN transformed_fields.value IS NULL THEN '[]'::jsonb + ELSE jsonb_build_array(transformed_fields.value) + END + END + ) FILTER (WHERE key IS NOT NULL), + '{}'::jsonb + )AS enums, + ( + SELECT string_agg(value::text, ' ') + FROM jsonb_each_text(e.data) + WHERE key IN ( + SELECT jsonb_object_keys(f.indexed_strings) + FROM families_indexed_fields f + WHERE f.family_id = c.family_id + ) + ) AS indexed_string_values FROM entities e JOIN categories c ON e.category_id = c.id LEFT JOIN entity_tags et ON e.id = et.entity_id @@ -38,6 +83,17 @@ direct_locations AS ( SELECT value, ordinality FROM jsonb_array_elements(e.locations) WITH ORDINALITY AS location(value, ordinality) ) AS location ON true + LEFT JOIN LATERAL ( + SELECT + key, + value + FROM jsonb_each(e.data) + WHERE key IN ( + SELECT jsonb_object_keys(f.indexed_enums) + FROM families_indexed_fields f + WHERE f.family_id = c.family_id + ) + ) AS transformed_fields ON true WHERE e.moderated GROUP BY e.id, c.family_id, e.display_name, e.category_id, location.value, location.ordinality ) @@ -57,7 +113,8 @@ SELECT NULL AS parent_id, NULL AS parent_display_name, dl.hidden, - to_tsvector(dl.display_name) AS full_text_search_ts + to_tsvector(dl.display_name || ' ' || COALESCE(dl.indexed_string_values, '')) AS full_text_search_ts, + dl.enums FROM direct_locations dl UNION @@ -78,7 +135,8 @@ SELECT tl.parent_id, tl.parent_display_name, dl.hidden, - to_tsvector(dl.display_name) AS full_text_search_ts + to_tsvector(dl.display_name || ' ' || COALESCE(dl.indexed_string_values, '')) AS full_text_search_ts, + dl.enums FROM transitive_locations tl JOIN direct_locations dl ON tl.child_id = dl.entity_id; @@ -88,9 +146,12 @@ CREATE UNIQUE INDEX entities_caches_id_idx ON entities_caches(id); -- Create indexes on the materialized view CREATE INDEX entities_caches_entity_id_idx ON entities_caches(entity_id); CREATE INDEX entities_caches_category_id_idx ON entities_caches(category_id); +CREATE INDEX entities_caches_family_id_idx ON entities_caches(family_id); +CREATE INDEX entities_caches_hidden_idx ON entities_caches (hidden); +CREATE INDEX entities_caches_enums_idx ON entities_caches USING GIN (enums); CREATE INDEX entities_caches_gps_location_idx ON entities_caches USING GIST((ST_SetSRID(ST_MakePoint(longitude, latitude), 4326))); CREATE INDEX entities_caches_web_mercator_location_idx ON entities_caches USING GIST(web_mercator_location); -CREATE INDEX entities_caches_full_text_search_idx ON entities_caches USING GIN(full_text_search_ts); +CREATE INDEX entities_caches_full_text_search_idx ON entities_caches USING GIST(full_text_search_ts); -- Function to refresh the materialized view CREATE OR REPLACE FUNCTION refresh_entities_caches() RETURNS void AS $$ diff --git a/backend/migrations/20240322224255_requests_for_entities_cache.sql b/backend/migrations/20240322224255_requests_for_entities_cache.sql index 0b1029a..e05c93b 100644 --- a/backend/migrations/20240322224255_requests_for_entities_cache.sql +++ b/backend/migrations/20240322224255_requests_for_entities_cache.sql @@ -17,7 +17,9 @@ CREATE OR REPLACE FUNCTION fetch_entities_within_view( active_categories_ids UUID[], required_tags_ids UUID[], - excluded_tags_ids UUID[] + excluded_tags_ids UUID[], + + enum_constraints JSONB ) RETURNS TABLE ( id UUID, entity_id UUID, @@ -65,6 +67,18 @@ BEGIN AND (ec.category_id = ANY(active_categories_ids)) AND (array_length(required_tags_ids, 1) = 0 OR required_tags_ids <@ ec.tags_ids) AND NOT (ec.tags_ids && excluded_tags_ids) + -- Enum constraints + AND ( + enum_constraints IS NULL OR + enum_constraints = '{}'::jsonb OR + ( + SELECT bool_and( + ec.enums->key ?| array(SELECT jsonb_array_elements_text(value)) + ) + FROM jsonb_each(enum_constraints) AS constraints(key, value) + WHERE key IS NOT NULL AND ec.enums ? key + ) + ) ), parent_entities AS ( SELECT @@ -143,7 +157,9 @@ CREATE OR REPLACE FUNCTION search_entities( required_tags_ids UUID[], excluded_tags_ids UUID[], - require_locations BOOL + require_locations BOOL, + + enum_constraints JSONB ) RETURNS TABLE ( id UUID, entity_id UUID, @@ -185,6 +201,18 @@ BEGIN AND NOT (ec.tags_ids && excluded_tags_ids) -- If we do not require locations, we only return entities with locations AND (NOT require_locations OR ec.web_mercator_location IS NOT NULL) + -- Enum constraints + AND ( + enum_constraints IS NULL OR + enum_constraints = '{}'::jsonb OR + ( + SELECT bool_and( + ec.enums->key ?| array(SELECT jsonb_array_elements_text(value)) + ) + FROM jsonb_each(enum_constraints) AS constraints(key, value) + WHERE key IS NOT NULL AND ec.enums ? key + ) + ) ORDER BY ts_rank(full_text_search_ts, plainto_tsquery(search_query)) DESC, (ec.web_mercator_location IS NOT NULL) DESC -- prioritize entities with locations @@ -229,7 +257,9 @@ CREATE OR REPLACE FUNCTION search_entities_admin( active_categories_ids UUID[], required_tags_ids UUID[], - excluded_tags_ids UUID[] + excluded_tags_ids UUID[], + + enum_constraints JSONB ) RETURNS TABLE ( id UUID, entity_id UUID, @@ -256,8 +286,21 @@ BEGIN ) AND ec.family_id = input_family_id AND (ec.category_id = ANY(active_categories_ids)) + -- Categories and tags constraints AND (array_length(required_tags_ids, 1) = 0 OR required_tags_ids <@ ec.tags_ids) AND NOT (ec.tags_ids && excluded_tags_ids) + -- Enum constraints + AND ( + enum_constraints IS NULL OR + enum_constraints = '{}'::jsonb OR + ( + SELECT bool_and( + ec.enums->key ?| array(SELECT jsonb_array_elements_text(value)) + ) + FROM jsonb_each(enum_constraints) AS constraints(key, value) + WHERE key IS NOT NULL AND ec.enums ? key + ) + ) ORDER BY ts_rank(full_text_search_ts, plainto_tsquery(search_query)) DESC ), diff --git a/backend/src/api/admin/entities.rs b/backend/src/api/admin/entities.rs index 073fed1..4f1d756 100644 --- a/backend/src/api/admin/entities.rs +++ b/backend/src/api/admin/entities.rs @@ -32,6 +32,7 @@ pub struct AdminSearchRequest { pub active_categories_ids: Vec, pub required_tags_ids: Vec, pub excluded_tags_ids: Vec, + pub enums_constraints: Value, } #[derive(FromRow, Deserialize, Serialize, ToSchema, Debug)] @@ -84,6 +85,7 @@ pub async fn admin_entities_search( active_categories_ids: search_req.active_categories_ids, required_tags_ids: search_req.required_tags_ids, excluded_tags_ids: search_req.excluded_tags_ids, + enums_constraints: search_req.enums_constraints, }, &mut conn, ) diff --git a/backend/src/api/map.rs b/backend/src/api/map.rs index 15bece4..1f80f57 100644 --- a/backend/src/api/map.rs +++ b/backend/src/api/map.rs @@ -12,6 +12,7 @@ use axum::{ Json, }; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::fmt::Display; use utoipa::ToSchema; use uuid::Uuid; @@ -38,6 +39,7 @@ pub struct ViewRequest { active_categories: Vec, active_required_tags: Vec, active_hidden_tags: Vec, + enums_constraints: Value, } impl Display for ViewRequest { @@ -125,6 +127,7 @@ pub async fn viewer_view_request( active_categories: request.active_categories, active_required_tags: request.active_required_tags, active_hidden_tags: request.active_hidden_tags, + enums_constraints: request.enums_constraints, }; Ok(AppJson( @@ -142,6 +145,7 @@ pub struct SearchRequest { active_required_tags: Vec, active_hidden_tags: Vec, require_locations: bool, + enums_constraints: Value, } impl Display for SearchRequest { @@ -189,6 +193,7 @@ async fn viewer_search_request( active_required_tags: request.active_required_tags, active_hidden_tags: request.active_hidden_tags, require_locations: request.require_locations, + enums_constraints: request.enums_constraints, }; Ok(AppJson( diff --git a/backend/src/api/root.rs b/backend/src/api/root.rs index 895fc64..4a842c4 100644 --- a/backend/src/api/root.rs +++ b/backend/src/api/root.rs @@ -98,7 +98,7 @@ async fn bootstrap( let signed_token = app_state.generate_token(MapUserTokenClaims { iat: Utc::now().timestamp() as usize, - exp: (Utc::now() + TimeDelta::try_minutes(1).expect("valid duration")).timestamp() as usize, + exp: (Utc::now() + TimeDelta::try_hours(1).expect("valid duration")).timestamp() as usize, perms: perms.clone(), }); tracing::trace!("Generated access token"); diff --git a/backend/src/models/entity_cache.rs b/backend/src/models/entity_cache.rs index 12c33b6..2d6c86c 100644 --- a/backend/src/models/entity_cache.rs +++ b/backend/src/models/entity_cache.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use crate::api::AppError; use serde::{Deserialize, Serialize}; +use serde_json::Value; use sqlx::{query_as, PgConnection}; use utoipa::ToSchema; use uuid::Uuid; @@ -202,6 +203,8 @@ pub struct FindEntitiesRequest { pub active_categories: Vec, pub active_required_tags: Vec, pub active_hidden_tags: Vec, + + pub enums_constraints: Value, } pub struct SearchEntitiesRequest { @@ -225,6 +228,8 @@ pub struct SearchEntitiesRequest { pub active_hidden_tags: Vec, pub require_locations: bool, + + pub enums_constraints: Value, } impl ViewerCachedEntity { @@ -253,7 +258,25 @@ impl ViewerCachedEntity { cluster_id, cluster_center_x, cluster_center_y - FROM fetch_entities_within_view($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) + FROM fetch_entities_within_view( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17 + ) "#, request.xmin, request.ymin, @@ -270,7 +293,8 @@ impl ViewerCachedEntity { request.cluster_params.map(|(_, min)| min).unwrap_or(0), &request.active_categories, &request.active_required_tags, - &request.active_hidden_tags + &request.active_hidden_tags, + &request.enums_constraints ) .fetch_all(conn) .await @@ -353,7 +377,23 @@ impl ViewerCachedEntity { total_results as "total_results!", total_pages as "total_pages!", response_current_page as "response_current_page!" - FROM search_entities($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) + FROM search_entities( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15 + ) "#, request.search_query, request.family_id, @@ -368,7 +408,8 @@ impl ViewerCachedEntity { &request.active_categories, &request.active_required_tags, &request.active_hidden_tags, - request.require_locations + request.require_locations, + &request.enums_constraints ) .fetch_all(conn) .await @@ -386,6 +427,7 @@ pub struct AdminSearchEntitiesRequest { pub active_categories_ids: Vec, pub required_tags_ids: Vec, pub excluded_tags_ids: Vec, + pub enums_constraints: Value, } impl AdminCachedEntity { @@ -407,7 +449,16 @@ impl AdminCachedEntity { total_pages as "total_pages!", response_current_page as "response_current_page!", hidden as "hidden!" - FROM search_entities_admin($1, $2, $3, $4, $5, $6, $7) + FROM search_entities_admin( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8 + ) "#, request.search_query, request.family_id, @@ -415,7 +466,8 @@ impl AdminCachedEntity { request.page_size, &request.active_categories_ids, &request.required_tags_ids, - &request.excluded_tags_ids + &request.excluded_tags_ids, + &request.enums_constraints ) .fetch_all(conn) .await diff --git a/frontend/components/admin/input/EntitySelect.vue b/frontend/components/admin/input/EntitySelect.vue index 5d7af52..f6bb35e 100644 --- a/frontend/components/admin/input/EntitySelect.vue +++ b/frontend/components/admin/input/EntitySelect.vue @@ -89,6 +89,7 @@ @@ -97,7 +98,13 @@