562 lines
24 KiB
PL/PgSQL
562 lines
24 KiB
PL/PgSQL
-- =============================================================================
|
|
-- Migration: 014_primitive_classification_system.sql
|
|
-- Purpose: Create primitive classification system for context-aware review analysis
|
|
-- =============================================================================
|
|
--
|
|
-- This migration introduces a "primitive" classification system that allows
|
|
-- industry-specific and category-specific configuration of what aspects to
|
|
-- look for when classifying reviews.
|
|
--
|
|
-- Components:
|
|
-- 1. pipeline.primitives - Frozen dictionary of primitives (quality dimensions)
|
|
-- 2. ALTER public.gbp_categories - Add primitive_configs and business_context
|
|
-- 3. pipeline.jsonb_deep_merge() - Recursive JSONB merge function
|
|
-- 4. pipeline.resolve_primitive_config() - Resolve configs through category tree
|
|
-- 5. pipeline.get_classification_context() - Get full classification context
|
|
--
|
|
-- Date: 2026-01-31
|
|
-- =============================================================================
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 1: PRIMITIVES TABLE (Frozen Dictionary)
|
|
-- =============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS pipeline.primitives (
|
|
code VARCHAR(30) PRIMARY KEY,
|
|
dimension VARCHAR(20) NOT NULL, -- quality, service, process, environment, value, trust, resolution, loyalty, escape
|
|
name VARCHAR(100) NOT NULL,
|
|
definition TEXT NOT NULL,
|
|
is_meta BOOLEAN DEFAULT FALSE, -- true for always-active primitives (HONESTY, ETHICS, etc.)
|
|
base_positive_signals TEXT[],
|
|
base_negative_signals TEXT[],
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
-- Index on dimension for filtering
|
|
CREATE INDEX IF NOT EXISTS idx_primitives_dimension ON pipeline.primitives(dimension);
|
|
|
|
-- Index on is_meta for quick access to always-active primitives
|
|
CREATE INDEX IF NOT EXISTS idx_primitives_is_meta ON pipeline.primitives(is_meta) WHERE is_meta = TRUE;
|
|
|
|
COMMENT ON TABLE pipeline.primitives IS 'Frozen dictionary of classification primitives (quality dimensions)';
|
|
COMMENT ON COLUMN pipeline.primitives.code IS 'Unique identifier for the primitive (e.g., FOOD_TASTE, SERVICE_SPEED)';
|
|
COMMENT ON COLUMN pipeline.primitives.dimension IS 'Category of the primitive (quality, service, process, etc.)';
|
|
COMMENT ON COLUMN pipeline.primitives.is_meta IS 'If true, this primitive is always active regardless of category config';
|
|
COMMENT ON COLUMN pipeline.primitives.base_positive_signals IS 'Default positive signal keywords for this primitive';
|
|
COMMENT ON COLUMN pipeline.primitives.base_negative_signals IS 'Default negative signal keywords for this primitive';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 2: ALTER gbp_categories TABLE
|
|
-- =============================================================================
|
|
|
|
-- Add primitive_configs column (JSONB for flexible config)
|
|
ALTER TABLE public.gbp_categories
|
|
ADD COLUMN IF NOT EXISTS primitive_configs JSONB DEFAULT '{}';
|
|
|
|
-- Add business_context column (JSONB for industry-specific context)
|
|
ALTER TABLE public.gbp_categories
|
|
ADD COLUMN IF NOT EXISTS business_context JSONB DEFAULT '{}';
|
|
|
|
-- Add config versioning columns
|
|
ALTER TABLE public.gbp_categories
|
|
ADD COLUMN IF NOT EXISTS config_version VARCHAR(20);
|
|
|
|
ALTER TABLE public.gbp_categories
|
|
ADD COLUMN IF NOT EXISTS config_generated_by VARCHAR(100);
|
|
|
|
ALTER TABLE public.gbp_categories
|
|
ADD COLUMN IF NOT EXISTS config_updated_at TIMESTAMP WITH TIME ZONE;
|
|
|
|
-- GIN indexes for JSONB containment queries
|
|
CREATE INDEX IF NOT EXISTS idx_gbp_categories_primitive_configs
|
|
ON public.gbp_categories USING GIN (primitive_configs);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_gbp_categories_business_context
|
|
ON public.gbp_categories USING GIN (business_context);
|
|
|
|
-- Index for config version lookups
|
|
CREATE INDEX IF NOT EXISTS idx_gbp_categories_config_version
|
|
ON public.gbp_categories(config_version) WHERE config_version IS NOT NULL;
|
|
|
|
COMMENT ON COLUMN public.gbp_categories.primitive_configs IS 'JSONB config for primitives at this category level (inherits from ancestors)';
|
|
COMMENT ON COLUMN public.gbp_categories.business_context IS 'JSONB business context for this category (industry-specific terminology, etc.)';
|
|
COMMENT ON COLUMN public.gbp_categories.config_version IS 'Version of the primitive config (for cache invalidation)';
|
|
COMMENT ON COLUMN public.gbp_categories.config_generated_by IS 'Tool/model that generated this config';
|
|
COMMENT ON COLUMN public.gbp_categories.config_updated_at IS 'When the config was last updated';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 3: JSONB DEEP MERGE FUNCTION
|
|
-- =============================================================================
|
|
|
|
-- Recursive JSONB merge function
|
|
-- - Objects: recursively merge (override wins on conflicts)
|
|
-- - Arrays: union with dedup by default
|
|
-- - If object has "__replace__": true, replace entirely instead of merge
|
|
CREATE OR REPLACE FUNCTION pipeline.jsonb_deep_merge(
|
|
base JSONB,
|
|
override JSONB
|
|
) RETURNS JSONB AS $$
|
|
DECLARE
|
|
result JSONB;
|
|
key TEXT;
|
|
base_value JSONB;
|
|
override_value JSONB;
|
|
merged_array JSONB;
|
|
BEGIN
|
|
-- Handle NULL cases
|
|
IF base IS NULL THEN
|
|
RETURN override;
|
|
END IF;
|
|
|
|
IF override IS NULL THEN
|
|
RETURN base;
|
|
END IF;
|
|
|
|
-- If override has __replace__ flag, return override without the flag
|
|
IF jsonb_typeof(override) = 'object' AND override ? '__replace__' AND (override->>'__replace__')::boolean = true THEN
|
|
RETURN override - '__replace__';
|
|
END IF;
|
|
|
|
-- If both are not objects, override wins
|
|
IF jsonb_typeof(base) != 'object' OR jsonb_typeof(override) != 'object' THEN
|
|
RETURN override;
|
|
END IF;
|
|
|
|
-- Both are objects, merge recursively
|
|
result := base;
|
|
|
|
FOR key IN SELECT jsonb_object_keys(override)
|
|
LOOP
|
|
override_value := override->key;
|
|
|
|
IF NOT (base ? key) THEN
|
|
-- Key doesn't exist in base, just add it
|
|
result := result || jsonb_build_object(key, override_value);
|
|
ELSE
|
|
base_value := base->key;
|
|
|
|
-- Check for __replace__ flag in the override value
|
|
IF jsonb_typeof(override_value) = 'object'
|
|
AND override_value ? '__replace__'
|
|
AND (override_value->>'__replace__')::boolean = true THEN
|
|
-- Replace entirely (without the __replace__ flag)
|
|
result := result || jsonb_build_object(key, override_value - '__replace__');
|
|
|
|
-- If both are objects, recurse
|
|
ELSIF jsonb_typeof(base_value) = 'object' AND jsonb_typeof(override_value) = 'object' THEN
|
|
result := result || jsonb_build_object(
|
|
key,
|
|
pipeline.jsonb_deep_merge(base_value, override_value)
|
|
);
|
|
|
|
-- If both are arrays, union with dedup
|
|
ELSIF jsonb_typeof(base_value) = 'array' AND jsonb_typeof(override_value) = 'array' THEN
|
|
-- Union arrays, remove duplicates
|
|
-- Using a subquery to deduplicate
|
|
SELECT jsonb_agg(DISTINCT elem)
|
|
INTO merged_array
|
|
FROM (
|
|
SELECT jsonb_array_elements(base_value) AS elem
|
|
UNION
|
|
SELECT jsonb_array_elements(override_value) AS elem
|
|
) AS combined;
|
|
|
|
result := result || jsonb_build_object(key, COALESCE(merged_array, '[]'::jsonb));
|
|
|
|
-- Otherwise, override wins
|
|
ELSE
|
|
result := result || jsonb_build_object(key, override_value);
|
|
END IF;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
RETURN result;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
COMMENT ON FUNCTION pipeline.jsonb_deep_merge(JSONB, JSONB) IS
|
|
'Recursively merges two JSONB objects. Objects are merged recursively (override wins on conflicts).
|
|
Arrays are unioned with dedup. Use {"__replace__": true, ...} to replace instead of merge.';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 4: RESOLVE PRIMITIVE CONFIG FUNCTION
|
|
-- =============================================================================
|
|
|
|
-- Resolves primitive config by merging ancestor configs (general -> specific)
|
|
CREATE OR REPLACE FUNCTION pipeline.resolve_primitive_config(
|
|
p_path ltree
|
|
) RETURNS JSONB AS $$
|
|
DECLARE
|
|
result JSONB := '{}';
|
|
row_config JSONB;
|
|
BEGIN
|
|
-- Fetch all ancestor nodes (including self), ordered by level ASC (general -> specific)
|
|
-- Uses @> operator: p_path is a descendant of (or equal to) the node's path
|
|
FOR row_config IN
|
|
SELECT primitive_configs
|
|
FROM public.gbp_categories
|
|
WHERE p_path <@ path -- p_path is descendant of or equal to path
|
|
ORDER BY level ASC
|
|
LOOP
|
|
-- Skip NULL or empty configs
|
|
IF row_config IS NOT NULL AND row_config != '{}' THEN
|
|
result := pipeline.jsonb_deep_merge(result, row_config);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
RETURN result;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION pipeline.resolve_primitive_config(ltree) IS
|
|
'Resolves the full primitive config for a category path by merging all ancestor configs from general to specific.';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 5: RESOLVE BUSINESS CONTEXT FUNCTION
|
|
-- =============================================================================
|
|
|
|
-- Resolves business context by merging ancestor contexts (general -> specific)
|
|
CREATE OR REPLACE FUNCTION pipeline.resolve_business_context(
|
|
p_path ltree
|
|
) RETURNS JSONB AS $$
|
|
DECLARE
|
|
result JSONB := '{}';
|
|
row_context JSONB;
|
|
BEGIN
|
|
-- Fetch all ancestor nodes (including self), ordered by level ASC (general -> specific)
|
|
FOR row_context IN
|
|
SELECT business_context
|
|
FROM public.gbp_categories
|
|
WHERE p_path <@ path
|
|
ORDER BY level ASC
|
|
LOOP
|
|
-- Skip NULL or empty contexts
|
|
IF row_context IS NOT NULL AND row_context != '{}' THEN
|
|
result := pipeline.jsonb_deep_merge(result, row_context);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
RETURN result;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION pipeline.resolve_business_context(ltree) IS
|
|
'Resolves the full business context for a category path by merging all ancestor contexts from general to specific.';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 6: GET CLASSIFICATION CONTEXT FUNCTION
|
|
-- =============================================================================
|
|
|
|
-- Returns complete classification context for a category path
|
|
CREATE OR REPLACE FUNCTION pipeline.get_classification_context(
|
|
p_path ltree
|
|
) RETURNS JSONB AS $$
|
|
DECLARE
|
|
resolved_primitives JSONB;
|
|
resolved_context JSONB;
|
|
primitives_dict JSONB;
|
|
BEGIN
|
|
-- Resolve the primitive config for this path
|
|
resolved_primitives := pipeline.resolve_primitive_config(p_path);
|
|
|
|
-- Resolve the business context for this path
|
|
resolved_context := pipeline.resolve_business_context(p_path);
|
|
|
|
-- Build the primitives dictionary from the primitives table
|
|
SELECT jsonb_object_agg(
|
|
code,
|
|
jsonb_build_object(
|
|
'code', code,
|
|
'dimension', dimension,
|
|
'name', name,
|
|
'definition', definition,
|
|
'is_meta', is_meta,
|
|
'base_positive_signals', COALESCE(to_jsonb(base_positive_signals), '[]'::jsonb),
|
|
'base_negative_signals', COALESCE(to_jsonb(base_negative_signals), '[]'::jsonb)
|
|
)
|
|
)
|
|
INTO primitives_dict
|
|
FROM pipeline.primitives;
|
|
|
|
-- Handle case where primitives table is empty
|
|
IF primitives_dict IS NULL THEN
|
|
primitives_dict := '{}'::jsonb;
|
|
END IF;
|
|
|
|
-- Return combined context object
|
|
RETURN jsonb_build_object(
|
|
'primitive_configs', resolved_primitives,
|
|
'business_context', resolved_context,
|
|
'primitives_dictionary', primitives_dict,
|
|
'category_path', p_path::text,
|
|
'resolved_at', NOW()
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION pipeline.get_classification_context(ltree) IS
|
|
'Returns complete classification context for a category path, including resolved primitive configs,
|
|
business context, and the full primitives dictionary.';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 7: HELPER FUNCTION - GET ACTIVE PRIMITIVES
|
|
-- =============================================================================
|
|
|
|
-- Returns the list of active primitive codes for a category path
|
|
-- (includes meta primitives + enabled primitives from config)
|
|
CREATE OR REPLACE FUNCTION pipeline.get_active_primitives(
|
|
p_path ltree
|
|
) RETURNS TEXT[] AS $$
|
|
DECLARE
|
|
resolved_config JSONB;
|
|
active_codes TEXT[];
|
|
meta_codes TEXT[];
|
|
config_enabled TEXT[];
|
|
config_disabled TEXT[];
|
|
BEGIN
|
|
-- Get resolved config
|
|
resolved_config := pipeline.resolve_primitive_config(p_path);
|
|
|
|
-- Get all meta primitives (always active)
|
|
SELECT array_agg(code)
|
|
INTO meta_codes
|
|
FROM pipeline.primitives
|
|
WHERE is_meta = TRUE;
|
|
|
|
-- Get enabled primitives from config
|
|
IF resolved_config ? 'enabled' THEN
|
|
SELECT array_agg(elem::text)
|
|
INTO config_enabled
|
|
FROM jsonb_array_elements_text(resolved_config->'enabled') AS elem;
|
|
END IF;
|
|
|
|
-- Get disabled primitives from config
|
|
IF resolved_config ? 'disabled' THEN
|
|
SELECT array_agg(elem::text)
|
|
INTO config_disabled
|
|
FROM jsonb_array_elements_text(resolved_config->'disabled') AS elem;
|
|
END IF;
|
|
|
|
-- Combine: meta + enabled, minus disabled
|
|
active_codes := COALESCE(meta_codes, ARRAY[]::TEXT[]) || COALESCE(config_enabled, ARRAY[]::TEXT[]);
|
|
|
|
-- Remove disabled primitives
|
|
IF config_disabled IS NOT NULL THEN
|
|
active_codes := array(
|
|
SELECT unnest(active_codes)
|
|
EXCEPT
|
|
SELECT unnest(config_disabled)
|
|
);
|
|
END IF;
|
|
|
|
-- Remove duplicates
|
|
active_codes := array(SELECT DISTINCT unnest(active_codes));
|
|
|
|
RETURN active_codes;
|
|
END;
|
|
$$ LANGUAGE plpgsql STABLE;
|
|
|
|
COMMENT ON FUNCTION pipeline.get_active_primitives(ltree) IS
|
|
'Returns array of active primitive codes for a category path (meta primitives + enabled - disabled).';
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 8: SEED INITIAL PRIMITIVES (Examples)
|
|
-- =============================================================================
|
|
|
|
-- Insert some example primitives (can be extended later)
|
|
INSERT INTO pipeline.primitives (code, dimension, name, definition, is_meta, base_positive_signals, base_negative_signals)
|
|
VALUES
|
|
-- Meta primitives (always active)
|
|
('HONESTY', 'trust', 'Honesty & Truthfulness', 'Whether the business is perceived as honest and truthful in their dealings', TRUE,
|
|
ARRAY['honest', 'truthful', 'transparent', 'upfront', 'no hidden'],
|
|
ARRAY['lied', 'dishonest', 'deceptive', 'misleading', 'hidden fees', 'bait and switch']),
|
|
|
|
('ETHICS', 'trust', 'Ethical Behavior', 'Whether the business behaves ethically and with integrity', TRUE,
|
|
ARRAY['ethical', 'integrity', 'fair', 'principled', 'moral'],
|
|
ARRAY['unethical', 'scam', 'fraud', 'cheat', 'ripoff', 'shady']),
|
|
|
|
('SAFETY', 'trust', 'Safety & Security', 'Whether customers feel safe and secure', TRUE,
|
|
ARRAY['safe', 'secure', 'protected', 'trust'],
|
|
ARRAY['unsafe', 'dangerous', 'security concern', 'risk', 'hazard']),
|
|
|
|
-- Quality dimension primitives
|
|
('FOOD_TASTE', 'quality', 'Food Taste & Flavor', 'Quality and taste of food items', FALSE,
|
|
ARRAY['delicious', 'tasty', 'flavorful', 'yummy', 'amazing taste', 'perfectly seasoned'],
|
|
ARRAY['bland', 'tasteless', 'bad taste', 'over-seasoned', 'under-seasoned', 'disgusting']),
|
|
|
|
('FOOD_FRESHNESS', 'quality', 'Food Freshness', 'Freshness of ingredients and food items', FALSE,
|
|
ARRAY['fresh', 'crisp', 'just made', 'homemade', 'organic'],
|
|
ARRAY['stale', 'old', 'not fresh', 'frozen', 'reheated', 'expired']),
|
|
|
|
('FOOD_PORTION', 'quality', 'Portion Size', 'Size and quantity of food servings', FALSE,
|
|
ARRAY['generous portions', 'large serving', 'filling', 'plenty of food'],
|
|
ARRAY['small portions', 'tiny', 'not enough', 'skimpy', 'overpriced for size']),
|
|
|
|
('PRODUCT_QUALITY', 'quality', 'Product Quality', 'Overall quality of products', FALSE,
|
|
ARRAY['high quality', 'well made', 'premium', 'durable', 'excellent quality'],
|
|
ARRAY['poor quality', 'cheap', 'flimsy', 'broke easily', 'defective']),
|
|
|
|
-- Service dimension primitives
|
|
('SERVICE_SPEED', 'service', 'Service Speed', 'Speed and timeliness of service', FALSE,
|
|
ARRAY['fast', 'quick', 'prompt', 'efficient', 'no wait'],
|
|
ARRAY['slow', 'long wait', 'took forever', 'delayed', 'waited too long']),
|
|
|
|
('SERVICE_FRIENDLINESS', 'service', 'Staff Friendliness', 'Friendliness and warmth of staff', FALSE,
|
|
ARRAY['friendly', 'welcoming', 'warm', 'nice', 'pleasant', 'smiled'],
|
|
ARRAY['rude', 'unfriendly', 'cold', 'dismissive', 'attitude', 'ignored']),
|
|
|
|
('SERVICE_KNOWLEDGE', 'service', 'Staff Knowledge', 'Knowledge and expertise of staff', FALSE,
|
|
ARRAY['knowledgeable', 'expert', 'helpful advice', 'knew their stuff', 'professional'],
|
|
ARRAY['clueless', 'didnt know', 'unhelpful', 'inexperienced', 'untrained']),
|
|
|
|
('SERVICE_ATTENTIVENESS', 'service', 'Staff Attentiveness', 'How attentive staff are to customer needs', FALSE,
|
|
ARRAY['attentive', 'checked on us', 'responsive', 'available', 'proactive'],
|
|
ARRAY['inattentive', 'ignored', 'couldnt find anyone', 'had to flag down', 'neglected']),
|
|
|
|
-- Environment dimension primitives
|
|
('ENV_CLEANLINESS', 'environment', 'Cleanliness', 'Cleanliness of the establishment', FALSE,
|
|
ARRAY['clean', 'spotless', 'tidy', 'well-maintained', 'hygienic'],
|
|
ARRAY['dirty', 'filthy', 'messy', 'gross', 'sticky', 'unhygienic']),
|
|
|
|
('ENV_AMBIANCE', 'environment', 'Ambiance & Atmosphere', 'Overall atmosphere and vibe', FALSE,
|
|
ARRAY['great atmosphere', 'nice ambiance', 'cozy', 'relaxing', 'beautiful decor'],
|
|
ARRAY['bad atmosphere', 'uncomfortable', 'loud', 'cramped', 'depressing']),
|
|
|
|
('ENV_PARKING', 'environment', 'Parking Availability', 'Availability and convenience of parking', FALSE,
|
|
ARRAY['easy parking', 'plenty of parking', 'free parking', 'valet available'],
|
|
ARRAY['no parking', 'hard to park', 'paid parking', 'had to park far']),
|
|
|
|
-- Value dimension primitives
|
|
('VALUE_PRICE', 'value', 'Price Level', 'Perception of price levels', FALSE,
|
|
ARRAY['affordable', 'reasonable prices', 'cheap', 'good deal', 'budget-friendly'],
|
|
ARRAY['expensive', 'overpriced', 'pricey', 'not worth the price', 'too costly']),
|
|
|
|
('VALUE_WORTH', 'value', 'Value for Money', 'Whether the experience is worth the cost', FALSE,
|
|
ARRAY['worth it', 'great value', 'bang for buck', 'money well spent'],
|
|
ARRAY['not worth it', 'waste of money', 'rip off', 'should be cheaper']),
|
|
|
|
-- Process dimension primitives
|
|
('PROCESS_BOOKING', 'process', 'Booking & Reservations', 'Ease of making reservations or appointments', FALSE,
|
|
ARRAY['easy to book', 'simple reservation', 'available appointments', 'online booking'],
|
|
ARRAY['hard to book', 'no availability', 'complicated booking', 'had to call multiple times']),
|
|
|
|
('PROCESS_WAIT', 'process', 'Wait Times', 'Time spent waiting for service', FALSE,
|
|
ARRAY['no wait', 'seated immediately', 'quick turnaround'],
|
|
ARRAY['long wait', 'waited forever', 'always busy', 'need to wait in line']),
|
|
|
|
-- Resolution dimension primitives
|
|
('RESOLUTION_RESPONSE', 'resolution', 'Problem Response', 'How problems and complaints are handled', FALSE,
|
|
ARRAY['fixed the issue', 'made it right', 'apologized', 'took responsibility'],
|
|
ARRAY['ignored complaint', 'didnt care', 'blamed me', 'no resolution', 'refused to help']),
|
|
|
|
-- Loyalty dimension primitives
|
|
('LOYALTY_RETURN', 'loyalty', 'Return Intent', 'Whether customers intend to return', FALSE,
|
|
ARRAY['will be back', 'coming back', 'regular customer', 'my go-to place'],
|
|
ARRAY['never again', 'wont return', 'last time', 'not coming back']),
|
|
|
|
('LOYALTY_RECOMMEND', 'loyalty', 'Recommendation Intent', 'Whether customers would recommend to others', FALSE,
|
|
ARRAY['highly recommend', 'tell everyone', 'bring friends', 'must try'],
|
|
ARRAY['dont recommend', 'avoid', 'stay away', 'warned friends']),
|
|
|
|
-- Escape dimension primitives (when customers leave early or avoid)
|
|
('ESCAPE_LEFT', 'escape', 'Early Departure', 'Whether customers left early or walked out', FALSE,
|
|
ARRAY[]::TEXT[], -- No positive signals for escape
|
|
ARRAY['walked out', 'left early', 'didnt finish', 'had to leave', 'couldnt stay'])
|
|
|
|
ON CONFLICT (code) DO UPDATE SET
|
|
dimension = EXCLUDED.dimension,
|
|
name = EXCLUDED.name,
|
|
definition = EXCLUDED.definition,
|
|
is_meta = EXCLUDED.is_meta,
|
|
base_positive_signals = EXCLUDED.base_positive_signals,
|
|
base_negative_signals = EXCLUDED.base_negative_signals;
|
|
|
|
|
|
-- =============================================================================
|
|
-- SECTION 9: EXAMPLE CATEGORY CONFIGS
|
|
-- =============================================================================
|
|
|
|
-- Example: Set primitive config for Food & Dining sector (level 1)
|
|
-- This would enable food-related primitives for all food businesses
|
|
UPDATE public.gbp_categories
|
|
SET
|
|
primitive_configs = '{
|
|
"enabled": ["FOOD_TASTE", "FOOD_FRESHNESS", "FOOD_PORTION", "SERVICE_SPEED", "SERVICE_FRIENDLINESS", "ENV_CLEANLINESS", "ENV_AMBIANCE", "VALUE_PRICE", "VALUE_WORTH", "PROCESS_WAIT"],
|
|
"weights": {
|
|
"FOOD_TASTE": 1.5,
|
|
"FOOD_FRESHNESS": 1.3,
|
|
"SERVICE_SPEED": 1.2
|
|
}
|
|
}'::jsonb,
|
|
business_context = '{
|
|
"terminology": {
|
|
"staff": ["server", "waiter", "waitress", "host", "hostess", "bartender"],
|
|
"product": ["food", "dish", "meal", "appetizer", "entree", "dessert", "drink"]
|
|
},
|
|
"industry": "food_service"
|
|
}'::jsonb,
|
|
config_version = 'v1.0.0',
|
|
config_generated_by = 'migration_014',
|
|
config_updated_at = NOW()
|
|
WHERE slug = 'food_dining' AND level = 1;
|
|
|
|
-- Example: Override config for Restaurants (level 2) - adds more specific settings
|
|
UPDATE public.gbp_categories
|
|
SET
|
|
primitive_configs = '{
|
|
"enabled": ["PROCESS_BOOKING", "ENV_PARKING"],
|
|
"weights": {
|
|
"PROCESS_WAIT": 1.3
|
|
},
|
|
"signals": {
|
|
"FOOD_TASTE": {
|
|
"positive": ["perfectly cooked", "chef special", "signature dish"],
|
|
"negative": ["undercooked", "overcooked", "cold food"]
|
|
}
|
|
}
|
|
}'::jsonb,
|
|
business_context = '{
|
|
"terminology": {
|
|
"staff": ["chef", "cook", "sous chef", "kitchen staff"]
|
|
},
|
|
"typical_visit_duration": "1-2 hours",
|
|
"reservation_common": true
|
|
}'::jsonb,
|
|
config_version = 'v1.0.0',
|
|
config_generated_by = 'migration_014',
|
|
config_updated_at = NOW()
|
|
WHERE slug = 'restaurants' AND level = 2;
|
|
|
|
|
|
-- =============================================================================
|
|
-- VERIFICATION QUERIES (can be removed in production)
|
|
-- =============================================================================
|
|
|
|
-- Verify primitives table
|
|
DO $$
|
|
BEGIN
|
|
RAISE NOTICE 'Primitives table created with % rows', (SELECT COUNT(*) FROM pipeline.primitives);
|
|
END $$;
|
|
|
|
-- Verify functions exist
|
|
DO $$
|
|
BEGIN
|
|
-- Test jsonb_deep_merge
|
|
ASSERT pipeline.jsonb_deep_merge('{"a": 1}'::jsonb, '{"b": 2}'::jsonb) = '{"a": 1, "b": 2}'::jsonb,
|
|
'jsonb_deep_merge basic test failed';
|
|
|
|
-- Test __replace__ flag
|
|
ASSERT pipeline.jsonb_deep_merge('{"a": {"x": 1, "y": 2}}'::jsonb, '{"a": {"__replace__": true, "z": 3}}'::jsonb) = '{"a": {"z": 3}}'::jsonb,
|
|
'jsonb_deep_merge __replace__ test failed';
|
|
|
|
RAISE NOTICE 'All function tests passed';
|
|
END $$;
|