Browse Source

feat: enhance Qdrant sync with detailed product descriptions and contact info #74

- Enhanced createProductText() to include all available metadata (categories, variants, attributes, meta descriptions)
- Added phone numbers and full addresses to order/customer Qdrant payloads
- Updated Qdrant collection indexes to include phone and address fields (city, country)
- Applied changes to both Shopify and WooCommerce sync functions
- Improved data richness for AI context and semantic search
Claude 5 months ago
parent
commit
a048f84923

+ 235 - 10
supabase/functions/_shared/qdrant-client.ts

@@ -318,6 +318,11 @@ export async function initializeStoreCollections(
         { field: 'status', type: 'keyword' },
         { field: 'total_price', type: 'float' },
         { field: 'customer_email', type: 'keyword' },
+        { field: 'customer_phone', type: 'keyword' },
+        { field: 'billing_city', type: 'keyword' },
+        { field: 'billing_country', type: 'keyword' },
+        { field: 'shipping_city', type: 'keyword' },
+        { field: 'shipping_country', type: 'keyword' },
       ]);
     }
   }
@@ -331,6 +336,9 @@ export async function initializeStoreCollections(
         { field: 'customer_id', type: 'keyword' },
         { field: 'platform', type: 'keyword' },
         { field: 'email', type: 'keyword' },
+        { field: 'phone', type: 'keyword' },
+        { field: 'city', type: 'keyword' },
+        { field: 'country', type: 'keyword' },
       ]);
     }
   }
@@ -352,35 +360,116 @@ export function generateSimpleEmbedding(text: string): number[] {
 }
 
 /**
- * Create text representation of product for embedding
+ * Create enhanced text representation of product for embedding
+ * Includes all available metadata and extra information
  */
 export function createProductText(product: any): string {
   const parts: string[] = [];
 
+  // Product title/name
   if (product.title || product.name) {
     parts.push(product.title || product.name);
   }
 
+  // Primary description (cleaned up HTML)
   if (product.description) {
-    // Truncate long descriptions
-    const desc = product.description.replace(/<[^>]*>/g, '').substring(0, 500);
-    parts.push(desc);
+    const desc = product.description.replace(/<[^>]*>/g, '').trim();
+    if (desc) {
+      parts.push(desc);
+    }
+  }
+
+  // Short description (WooCommerce)
+  if (product.short_description && product.short_description !== product.description) {
+    const shortDesc = product.short_description.replace(/<[^>]*>/g, '').trim();
+    if (shortDesc) {
+      parts.push(shortDesc);
+    }
   }
 
+  // SKU and product identifiers
   if (product.sku) {
     parts.push(`SKU: ${product.sku}`);
   }
 
+  // Vendor/Brand information
   if (product.vendor) {
     parts.push(`Vendor: ${product.vendor}`);
   }
 
+  // Product type/category
   if (product.product_type) {
     parts.push(`Type: ${product.product_type}`);
   }
 
+  // Categories (WooCommerce)
+  if (product.categories && Array.isArray(product.categories)) {
+    const categoryNames = product.categories
+      .map((c: any) => c.name || c)
+      .filter(Boolean);
+    if (categoryNames.length > 0) {
+      parts.push(`Categories: ${categoryNames.join(', ')}`);
+    }
+  }
+
+  // Tags
   if (product.tags && Array.isArray(product.tags)) {
-    parts.push(`Tags: ${product.tags.join(', ')}`);
+    const tagList = product.tags.filter(Boolean);
+    if (tagList.length > 0) {
+      parts.push(`Tags: ${tagList.join(', ')}`);
+    }
+  }
+
+  // Variant information
+  if (product.variants && Array.isArray(product.variants) && product.variants.length > 0) {
+    const variantInfo: string[] = [];
+    product.variants.forEach((variant: any) => {
+      if (variant.title && variant.title !== 'Default Title') {
+        variantInfo.push(variant.title);
+      }
+      if (variant.option1) variantInfo.push(variant.option1);
+      if (variant.option2) variantInfo.push(variant.option2);
+      if (variant.option3) variantInfo.push(variant.option3);
+    });
+    if (variantInfo.length > 0) {
+      parts.push(`Variants: ${[...new Set(variantInfo)].join(', ')}`);
+    }
+  }
+
+  // Product attributes (WooCommerce)
+  if (product.attributes && Array.isArray(product.attributes)) {
+    const attributes = product.attributes
+      .map((attr: any) => {
+        if (attr.name && attr.options) {
+          const options = Array.isArray(attr.options) ? attr.options.join(', ') : attr.options;
+          return `${attr.name}: ${options}`;
+        }
+        return null;
+      })
+      .filter(Boolean);
+    if (attributes.length > 0) {
+      parts.push(attributes.join(' | '));
+    }
+  }
+
+  // Meta description (ShopRenter/SEO)
+  if (product.meta_description && product.meta_description !== product.description) {
+    const metaDesc = product.meta_description.replace(/<[^>]*>/g, '').trim();
+    if (metaDesc) {
+      parts.push(metaDesc);
+    }
+  }
+
+  // Price information for context
+  if (product.price) {
+    parts.push(`Price: ${product.price}`);
+  }
+
+  // Stock status (helpful context)
+  if (product.stock_status) {
+    parts.push(`Stock: ${product.stock_status}`);
+  } else if (product.status) {
+    parts.push(`Status: ${product.status}`);
   }
 
   return parts.join(' | ');
@@ -388,6 +477,7 @@ export function createProductText(product: any): string {
 
 /**
  * Create text representation of order for embedding
+ * Includes customer contact information and addresses
  */
 export function createOrderText(order: any): string {
   const parts: string[] = [];
@@ -398,12 +488,60 @@ export function createOrderText(order: any): string {
     parts.push(`Customer: ${order.customer_name}`);
   }
 
-  if (order.total_price) {
-    parts.push(`Total: ${order.total_price} ${order.currency || ''}`);
+  // Customer email
+  if (order.customer_email || order.email) {
+    parts.push(`Email: ${order.customer_email || order.email}`);
+  }
+
+  // Customer phone
+  if (order.customer_phone || order.phone) {
+    parts.push(`Phone: ${order.customer_phone || order.phone}`);
+  }
+
+  // Billing address
+  if (order.billing_address) {
+    const addr = order.billing_address;
+    const addressParts = [
+      addr.address1,
+      addr.address2,
+      addr.city,
+      addr.province || addr.state,
+      addr.zip || addr.postcode,
+      addr.country
+    ].filter(Boolean);
+    if (addressParts.length > 0) {
+      parts.push(`Billing: ${addressParts.join(', ')}`);
+    }
+    if (addr.phone && addr.phone !== (order.customer_phone || order.phone)) {
+      parts.push(`Billing Phone: ${addr.phone}`);
+    }
+  }
+
+  // Shipping address
+  if (order.shipping_address) {
+    const addr = order.shipping_address;
+    const addressParts = [
+      addr.address1,
+      addr.address2,
+      addr.city,
+      addr.province || addr.state,
+      addr.zip || addr.postcode,
+      addr.country
+    ].filter(Boolean);
+    if (addressParts.length > 0) {
+      parts.push(`Shipping: ${addressParts.join(', ')}`);
+    }
+    if (addr.phone && addr.phone !== (order.customer_phone || order.phone)) {
+      parts.push(`Shipping Phone: ${addr.phone}`);
+    }
+  }
+
+  if (order.total_price || order.total) {
+    parts.push(`Total: ${order.total_price || order.total} ${order.currency || ''}`);
   }
 
-  if (order.financial_status) {
-    parts.push(`Payment: ${order.financial_status}`);
+  if (order.financial_status || order.status) {
+    parts.push(`Payment: ${order.financial_status || order.status}`);
   }
 
   if (order.fulfillment_status) {
@@ -417,11 +555,18 @@ export function createOrderText(order: any): string {
     parts.push(`Items: ${items}`);
   }
 
+  // Order note
+  if (order.note) {
+    const note = order.note.substring(0, 200); // Limit note length
+    parts.push(`Note: ${note}`);
+  }
+
   return parts.join(' | ');
 }
 
 /**
  * Create text representation of customer for embedding
+ * Includes contact information and addresses
  */
 export function createCustomerText(customer: any): string {
   const parts: string[] = [];
@@ -430,6 +575,10 @@ export function createCustomerText(customer: any): string {
     parts.push(`${customer.first_name || ''} ${customer.last_name || ''}`.trim());
   }
 
+  if (customer.username) {
+    parts.push(`Username: ${customer.username}`);
+  }
+
   if (customer.email) {
     parts.push(`Email: ${customer.email}`);
   }
@@ -438,6 +587,65 @@ export function createCustomerText(customer: any): string {
     parts.push(`Phone: ${customer.phone}`);
   }
 
+  // Default address or billing address
+  if (customer.default_address) {
+    const addr = customer.default_address;
+    const addressParts = [
+      addr.address1,
+      addr.address2,
+      addr.city,
+      addr.province || addr.state,
+      addr.zip || addr.postcode,
+      addr.country
+    ].filter(Boolean);
+    if (addressParts.length > 0) {
+      parts.push(`Address: ${addressParts.join(', ')}`);
+    }
+    if (addr.phone && addr.phone !== customer.phone) {
+      parts.push(`Address Phone: ${addr.phone}`);
+    }
+  } else if (customer.billing_address || customer.billing) {
+    const addr = customer.billing_address || customer.billing;
+    const addressParts = [
+      addr.address1,
+      addr.address2,
+      addr.city,
+      addr.province || addr.state,
+      addr.zip || addr.postcode,
+      addr.country
+    ].filter(Boolean);
+    if (addressParts.length > 0) {
+      parts.push(`Billing: ${addressParts.join(', ')}`);
+    }
+    if (addr.phone && addr.phone !== customer.phone) {
+      parts.push(`Billing Phone: ${addr.phone}`);
+    }
+  }
+
+  // Shipping address (if different from billing)
+  if (customer.shipping_address || customer.shipping) {
+    const addr = customer.shipping_address || customer.shipping;
+    const addressParts = [
+      addr.address1,
+      addr.address2,
+      addr.city,
+      addr.province || addr.state,
+      addr.zip || addr.postcode,
+      addr.country
+    ].filter(Boolean);
+    if (addressParts.length > 0) {
+      parts.push(`Shipping: ${addressParts.join(', ')}`);
+    }
+    if (addr.phone && addr.phone !== customer.phone) {
+      parts.push(`Shipping Phone: ${addr.phone}`);
+    }
+  }
+
+  // Additional addresses (Shopify)
+  if (customer.addresses && Array.isArray(customer.addresses) && customer.addresses.length > 1) {
+    parts.push(`Additional addresses: ${customer.addresses.length - 1}`);
+  }
+
   if (customer.orders_count) {
     parts.push(`Orders: ${customer.orders_count}`);
   }
@@ -446,8 +654,25 @@ export function createCustomerText(customer: any): string {
     parts.push(`Total spent: ${customer.total_spent} ${customer.currency || ''}`);
   }
 
+  if (customer.company) {
+    parts.push(`Company: ${customer.company}`);
+  }
+
   if (customer.tags && Array.isArray(customer.tags)) {
-    parts.push(`Tags: ${customer.tags.join(', ')}`);
+    const tagList = customer.tags.filter(Boolean);
+    if (tagList.length > 0) {
+      parts.push(`Tags: ${tagList.join(', ')}`);
+    }
+  }
+
+  // Customer state/status
+  if (customer.state) {
+    parts.push(`Status: ${customer.state}`);
+  }
+
+  // Marketing preferences
+  if (customer.accepts_marketing !== undefined) {
+    parts.push(`Marketing: ${customer.accepts_marketing ? 'Yes' : 'No'}`);
   }
 
   return parts.join(' | ');

+ 31 - 3
supabase/functions/shopify-sync/index.ts

@@ -164,6 +164,9 @@ async function syncProductsToQdrant(
         vendor: product.vendor,
         product_type: product.product_type,
         tags: product.tags ? product.tags.split(',').map(t => t.trim()) : [],
+        variants: product.variants,
+        price: primaryVariant?.price,
+        status: product.status,
       })
 
       return {
@@ -257,11 +260,18 @@ async function syncOrdersToQdrant(
         order_number: order.order_number,
         name: order.name,
         customer_name: order.customer ? `${order.customer.first_name || ''} ${order.customer.last_name || ''}`.trim() : null,
+        customer_email: order.customer?.email || order.email,
+        customer_phone: order.customer?.phone || order.billing_address?.phone,
+        email: order.email,
+        phone: order.customer?.phone || order.billing_address?.phone,
+        billing_address: order.billing_address,
+        shipping_address: order.shipping_address,
         total_price: order.current_total_price,
         currency: order.currency,
         financial_status: order.financial_status,
         fulfillment_status: order.fulfillment_status,
         line_items: order.line_items,
+        note: order.note,
       })
 
       return {
@@ -274,13 +284,21 @@ async function syncOrdersToQdrant(
           order_number: order.order_number.toString(),
           name: order.name,
           email: order.email || null,
+          customer_name: order.customer ? `${order.customer.first_name || ''} ${order.customer.last_name || ''}`.trim() : null,
+          customer_email: order.customer?.email || order.email || null,
+          customer_phone: order.customer?.phone || order.billing_address?.phone || null,
           phone: order.customer?.phone || order.billing_address?.phone || null,
           financial_status: order.financial_status,
           fulfillment_status: order.fulfillment_status || null,
           total_price: parseFloat(order.current_total_price) || 0,
           currency: order.currency,
-          customer_name: order.customer ? `${order.customer.first_name || ''} ${order.customer.last_name || ''}`.trim() : null,
-          customer_email: order.customer?.email || order.email || null,
+          billing_address: order.billing_address || null,
+          billing_city: order.billing_address?.city || null,
+          billing_country: order.billing_address?.country || null,
+          shipping_address: order.shipping_address || null,
+          shipping_city: order.shipping_address?.city || null,
+          shipping_country: order.shipping_address?.country || null,
+          note: order.note || null,
           synced_at: new Date().toISOString(),
         }
       }
@@ -352,10 +370,14 @@ async function syncCustomersToQdrant(
         first_name: customer.first_name,
         last_name: customer.last_name,
         email: customer.email,
-        phone: customer.phone,
+        phone: customer.phone || customer.default_address?.phone,
+        default_address: customer.default_address,
+        addresses: customer.addresses,
         orders_count: customer.orders_count,
         total_spent: customer.total_spent,
         currency: customer.currency,
+        state: customer.state,
+        accepts_marketing: customer.accepts_marketing,
         tags: customer.tags ? customer.tags.split(',').map(t => t.trim()) : [],
       })
 
@@ -370,10 +392,16 @@ async function syncCustomersToQdrant(
           first_name: customer.first_name || null,
           last_name: customer.last_name || null,
           phone: customer.phone || customer.default_address?.phone || null,
+          default_address: customer.default_address || null,
+          city: customer.default_address?.city || null,
+          country: customer.default_address?.country || null,
+          addresses: customer.addresses || [],
           orders_count: customer.orders_count || 0,
           total_spent: parseFloat(customer.total_spent) || 0,
           currency: customer.currency || 'USD',
           state: customer.state,
+          accepts_marketing: customer.accepts_marketing || false,
+          tags: customer.tags ? customer.tags.split(',').map(t => t.trim()) : [],
           synced_at: new Date().toISOString(),
         }
       }

+ 31 - 0
supabase/functions/woocommerce-sync/index.ts

@@ -180,8 +180,13 @@ async function syncProductsToQdrant(
       const productText = createProductText({
         name: product.name,
         description: product.description,
+        short_description: product.short_description,
         sku: product.sku,
+        categories: product.categories,
         tags: product.tags?.map((t: any) => t.name) || [],
+        attributes: product.attributes,
+        price: product.price,
+        stock_status: product.stock_status,
       })
 
       return {
@@ -269,10 +274,17 @@ async function syncOrdersToQdrant(
       const orderText = createOrderText({
         order_number: order.number || order.id,
         customer_name: `${order.billing?.first_name || ''} ${order.billing?.last_name || ''}`.trim(),
+        customer_email: order.billing?.email,
+        customer_phone: order.billing?.phone,
+        billing_address: order.billing,
+        shipping_address: order.shipping,
         total_price: order.total,
+        total: order.total,
         currency: order.currency,
+        status: order.status,
         financial_status: order.status,
         line_items: order.line_items,
+        note: order.customer_note,
       })
 
       return {
@@ -285,9 +297,19 @@ async function syncOrdersToQdrant(
           order_number: order.number || order.id.toString(),
           status: order.status,
           total: parseFloat(order.total) || 0,
+          total_price: parseFloat(order.total) || 0,
           currency: order.currency || 'USD',
           customer_name: `${order.billing?.first_name || ''} ${order.billing?.last_name || ''}`.trim(),
           customer_email: order.billing?.email || null,
+          customer_phone: order.billing?.phone || null,
+          phone: order.billing?.phone || null,
+          billing_address: order.billing || null,
+          billing_city: order.billing?.city || null,
+          billing_country: order.billing?.country || null,
+          shipping_address: order.shipping || null,
+          shipping_city: order.shipping?.city || null,
+          shipping_country: order.shipping?.country || null,
+          note: order.customer_note || null,
           synced_at: new Date().toISOString(),
         }
       }
@@ -358,7 +380,11 @@ async function syncCustomersToQdrant(
       const customerText = createCustomerText({
         first_name: customer.first_name,
         last_name: customer.last_name,
+        username: customer.username,
         email: customer.email,
+        phone: customer.billing?.phone,
+        billing: customer.billing,
+        shipping: customer.shipping,
         orders_count: customer.orders_count,
         total_spent: customer.total_spent,
       })
@@ -374,6 +400,11 @@ async function syncCustomersToQdrant(
           first_name: customer.first_name || null,
           last_name: customer.last_name || null,
           username: customer.username || null,
+          phone: customer.billing?.phone || customer.shipping?.phone || null,
+          billing_address: customer.billing || null,
+          shipping_address: customer.shipping || null,
+          city: customer.billing?.city || customer.shipping?.city || null,
+          country: customer.billing?.country || customer.shipping?.country || null,
           orders_count: customer.orders_count || 0,
           total_spent: parseFloat(customer.total_spent || '0'),
           synced_at: new Date().toISOString(),