Plugin Directory

Changeset 3763005


Ignore:
Timestamp:
10/24/2026 09:26:11 AM (6 hours ago)
Author:
cinghaman
Message:

Release 0.7.2: fast Woo catalog search on large shops, published-only SKU search, require PHP 8.0

Location:
agentmatica-mcp-connector
Files:
10 edited
1 copied

Legend:

Unmodified
Added
Removed
  • agentmatica-mcp-connector/tags/0.7.2/agentmatica.php

    r3736588 r3763005  
    44 * Plugin URI: https://agentmatica.com/docs/integrations/wordpress
    55 * Description: Free MCP connector for Agentmatica.com. Expose WordPress content and optional WooCommerce tools to Agentmatica visitor chat.
    6  * Version: 0.7.1
     6 * Version: 0.7.2
    77 * Requires at least: 6.9
    8  * Requires PHP: 7.4
     8 * Requires PHP: 8.0
    99 * Author: Thought Bulb
    1010 * Author URI: https://thoughtbulb.dev
    … …  
    2323}
    2424
    25 define( 'AGENTMATICA_VERSION', '0.7.1' );
     25define( 'AGENTMATICA_VERSION', '0.7.2' );
    2626define( 'AGENTMATICA_PLUGIN_FILE', __FILE__ );
    2727define( 'AGENTMATICA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • agentmatica-mcp-connector/tags/0.7.2/includes/class-ucp-catalog.php

    r3736423 r3763005  
    4444
    4545                if ( $query !== '' ) {
    46                         $products = self::query_published_products_by_search( $query, $limit );
     46                        $found    = self::query_published_products_by_search( $query, $limit );
     47                        $products = $found['products'];
     48                        $total    = $found['total'];
    4749                } else {
    48                         $products = self::query_all_published_products( $limit );
    49                 }
    50 
    51                 $total_count = self::count_catalog_products();
    52                 $payload     = array();
     50                        $found    = self::query_all_published_products( $limit );
     51                        $products = $found['products'];
     52                        $total    = $found['total'];
     53                }
     54
     55                $payload = array();
    5356
    5457                foreach ( $products as $product ) {
    … …  
    7376                }
    7477
    75                 return self::catalog_response( $payload, $total_count, $limit );
     78                return self::catalog_response( $payload, $total, $limit );
    7679        }
    7780
    … …  
    166169                        : $arguments;
    167170                $raw_id  = isset( $catalog['id'] ) ? (string) $catalog['id'] : '';
     171                if ( $raw_id === '' && isset( $arguments['id'] ) ) {
     172                        $raw_id = (string) $arguments['id'];
     173                }
    168174
    169175                $product = self::load_product_by_identifier( $raw_id );
    170176                if ( ! $product ) {
     177                        $name = '';
     178                        if ( isset( $catalog['name'] ) ) {
     179                                $name = sanitize_text_field( (string) $catalog['name'] );
     180                        } elseif ( isset( $arguments['name'] ) ) {
     181                                $name = sanitize_text_field( (string) $arguments['name'] );
     182                        } elseif ( isset( $catalog['query'] ) ) {
     183                                $name = sanitize_text_field( (string) $catalog['query'] );
     184                        }
     185
     186                        if ( $name !== '' ) {
     187                                return self::search_catalog(
     188                                        array(
     189                                                'query'      => $name,
     190                                                'pagination' => array( 'limit' => 5 ),
     191                                        )
     192                                );
     193                        }
     194
    171195                        return self::catalog_response( array() );
    172196                }
    … …  
    260284        }
    261285
    262         private static function count_catalog_products(): int {
    263                 if ( ! function_exists( 'wc_get_products' ) ) {
     286        /**
     287         * Published product count from post counts (no product hydration).
     288         */
     289        private static function count_published_products(): int {
     290                $counts = wp_count_posts( 'product' );
     291                if ( ! is_object( $counts ) || ! isset( $counts->publish ) ) {
    264292                        return 0;
    265293                }
    266294
    267                 $count = 0;
    268                 foreach ( self::load_published_product_objects( -1 ) as $product ) {
    269                         if ( self::is_catalog_available( $product ) && self::format_product( $product ) ) {
    270                                 ++$count;
    271                         }
    272                 }
    273 
    274                 return $count;
     295                return (int) $counts->publish;
    275296        }
    276297
    … …  
    308329         * Load published products for catalog browse (available to buy only).
    309330         *
    310          * @return list<WC_Product>
     331         * @return array{products: list<WC_Product>, total: int}
    311332         */
    312333        private static function query_all_published_products( int $limit ): array {
     334                $query = new WP_Query(
     335                        array(
     336                                'post_type'              => 'product',
     337                                'post_status'            => 'publish',
     338                                'posts_per_page'         => $limit,
     339                                'paged'                  => 1,
     340                                'orderby'                => 'date',
     341                                'order'                  => 'DESC',
     342                                'fields'                 => 'ids',
     343                                'no_found_rows'          => false,
     344                                'ignore_sticky_posts'    => true,
     345                                'update_post_meta_cache' => false,
     346                                'update_post_term_cache' => false,
     347                                'meta_query'             => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- instock browse only, paged.
     348                                        array(
     349                                                'key'   => '_stock_status',
     350                                                'value' => 'instock',
     351                                        ),
     352                                ),
     353                        )
     354                );
     355
    313356                $products = array();
    314                 foreach ( self::load_published_product_objects( -1 ) as $product ) {
     357                foreach ( $query->posts as $product_id ) {
     358                        $product = wc_get_product( (int) $product_id );
     359                        if ( ! $product instanceof WC_Product ) {
     360                                continue;
     361                        }
    315362                        if ( ! self::is_catalog_available( $product ) ) {
    316363                                continue;
    317364                        }
    318 
    319365                        $products[] = $product;
    320                         if ( count( $products ) >= $limit ) {
    321                                 break;
    322                         }
    323                 }
    324 
    325                 return $products;
    326         }
    327 
    328         /**
    329          * @return list<WC_Product>
    330          */
    331         private static function load_published_product_objects( int $limit ): array {
    332                 $ids = get_posts(
    333                         array(
    334                                 'post_type'      => 'product',
    335                                 'post_status'    => 'publish',
    336                                 'posts_per_page' => $limit,
    337                                 'orderby'        => 'date',
    338                                 'order'          => 'DESC',
    339                                 'fields'         => 'ids',
    340                         )
    341                 );
    342 
    343                 if ( ! is_array( $ids ) ) {
    344                         return array();
    345                 }
    346 
    347                 $products = array();
    348                 foreach ( $ids as $product_id ) {
    349                         $product = wc_get_product( (int) $product_id );
    350                         if ( $product instanceof WC_Product ) {
    351                                 $products[] = $product;
    352                         }
    353                 }
    354 
    355                 return $products;
    356         }
    357 
    358         /**
    359          * Named search — may include unavailable matches so the agent can explain stock status.
     366                }
     367
     368                $total = (int) $query->found_posts;
     369                if ( $total < 1 ) {
     370                        $total = self::count_published_products();
     371                }
     372
     373                return array(
     374                        'products' => $products,
     375                        'total'    => $total,
     376                );
     377        }
     378
     379        /**
     380         * Named search — SQL/Woo search plus a capped PHP re-rank. Never hydrates the full catalogue.
    360381         *
    361          * @return list<WC_Product>
     382         * @return array{products: list<WC_Product>, total: int}
    362383         */
    363384        private static function query_published_products_by_search( string $search, int $limit ): array {
    364385                $normalized = strtolower( trim( preg_replace( '/\s+/', ' ', $search ) ) );
    365386                if ( $normalized === '' ) {
    366                         return array();
    367                 }
     387                        return array(
     388                                'products' => array(),
     389                                'total'    => 0,
     390                        );
     391                }
     392
     393                $candidate_limit = min( 100, max( $limit * 5, 40 ) );
     394                $found           = self::search_published_product_ids( $normalized, $candidate_limit );
     395                $ids             = $found['ids'];
     396                $total           = $found['total'];
    368397
    369398                $scored = array();
    370 
    371                 foreach ( self::load_published_product_objects( -1 ) as $product ) {
     399                foreach ( $ids as $product_id ) {
     400                        $product = wc_get_product( $product_id );
     401                        if ( ! $product instanceof WC_Product ) {
     402                                continue;
     403                        }
     404
    372405                        $score = Agentmatica_Catalog_Search::product_match_score( $product, $search );
    373406                        if ( $score <= 0 ) {
    374                                 continue;
     407                                $score = 10;
    375408                        }
    376409
    377410                        $scored[] = array(
    378411                                'score'   => $score,
     412                                'id'      => $product_id,
    379413                                'product' => $product,
    380414                        );
    … …  
    384418                        $scored,
    385419                        static function ( array $left, array $right ): int {
    386                                 return $right['score'] <=> $left['score'];
     420                                $cmp = $right['score'] <=> $left['score'];
     421                                if ( 0 !== $cmp ) {
     422                                        return $cmp;
     423                                }
     424
     425                                return $left['id'] <=> $right['id'];
    387426                        }
    388427                );
    … …  
    396435                }
    397436
    398                 return $products;
     437                if ( $total < count( $products ) ) {
     438                        $total = count( $products );
     439                }
     440
     441                return array(
     442                        'products' => $products,
     443                        'total'    => $total,
     444                );
     445        }
     446
     447        /**
     448         * @return array{ids: list<int>, total: int}
     449         */
     450        private static function search_published_product_ids( string $search, int $limit ): array {
     451                $ids   = array();
     452                $total = 0;
     453
     454                $sku_id = function_exists( 'wc_get_product_id_by_sku' ) ? wc_get_product_id_by_sku( $search ) : 0;
     455                if ( $sku_id ) {
     456                        $sku_product = wc_get_product( $sku_id );
     457                        if ( $sku_product instanceof WC_Product_Variation ) {
     458                                $sku_id = (int) $sku_product->get_parent_id();
     459                        }
     460                        // wc_get_product_id_by_sku() matches drafts and private products too.
     461                        if ( $sku_id > 0 && get_post_status( $sku_id ) === 'publish' ) {
     462                                $ids[] = $sku_id;
     463                        }
     464                }
     465
     466                $query = new WP_Query(
     467                        array(
     468                                'post_type'              => 'product',
     469                                'post_status'            => 'publish',
     470                                'posts_per_page'         => $limit,
     471                                's'                      => $search,
     472                                'fields'                 => 'ids',
     473                                'no_found_rows'          => false,
     474                                'ignore_sticky_posts'    => true,
     475                                'update_post_meta_cache' => false,
     476                                'update_post_term_cache' => false,
     477                        )
     478                );
     479
     480                foreach ( $query->posts as $product_id ) {
     481                        $ids[] = (int) $product_id;
     482                }
     483                $total = max( $total, (int) $query->found_posts, count( $ids ) );
     484
     485                $title_found = self::search_product_ids_by_title_tokens( $search, $limit );
     486                $ids         = array_merge( $title_found['ids'], $ids );
     487                $total       = max( $total, $title_found['total'] );
     488
     489                $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
     490
     491                return array(
     492                        'ids'   => $ids,
     493                        'total' => $total,
     494                );
     495        }
     496
     497        /**
     498         * Title AND-match for all query tokens (older fixtures WP `s=` ranking can drop).
     499         *
     500         * @return array{ids: list<int>, total: int}
     501         */
     502        private static function search_product_ids_by_title_tokens( string $search, int $limit ): array {
     503                global $wpdb;
     504
     505                $words  = preg_split( '/\s+/', $search );
     506                $tokens = array();
     507                foreach ( false === $words ? array() : $words as $word ) {
     508                        $word = trim( (string) $word );
     509                        if ( strlen( $word ) < 3 ) {
     510                                continue;
     511                        }
     512                        $tokens[] = $word;
     513                }
     514                $tokens = array_values( array_unique( $tokens ) );
     515                if ( $tokens === array() ) {
     516                        return array(
     517                                'ids'   => array(),
     518                                'total' => 0,
     519                        );
     520                }
     521
     522                $posts      = $wpdb->posts;
     523                $conditions = array( "post_type = 'product'", "post_status = 'publish'" );
     524                $values     = array();
     525                foreach ( $tokens as $token ) {
     526                        $conditions[] = 'post_title LIKE %s';
     527                        $values[]     = '%' . $wpdb->esc_like( $token ) . '%';
     528                }
     529
     530                $where    = implode( ' AND ', $conditions );
     531                $values[] = $limit;
     532
     533                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- `$posts` is `$wpdb->posts`; `$where` is LIKE %s placeholders.
     534                $ids = $wpdb->get_col(
     535                        $wpdb->prepare(
     536                                "SELECT ID FROM {$posts} WHERE {$where} ORDER BY ID ASC LIMIT %d",
     537                                ...$values
     538                        )
     539                );
     540
     541                $count_values = array_slice( $values, 0, -1 );
     542                $count        = $wpdb->get_var(
     543                        $wpdb->prepare(
     544                                "SELECT COUNT(ID) FROM {$posts} WHERE {$where}",
     545                                ...$count_values
     546                        )
     547                );
     548                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
     549
     550                return array(
     551                        'ids'   => array_map( 'intval', is_array( $ids ) ? $ids : array() ),
     552                        'total' => (int) $count,
     553                );
    399554        }
    400555
  • agentmatica-mcp-connector/tags/0.7.2/includes/modules/woocommerce/class-cart.php

    r3736423 r3763005  
    2020
    2121        public static function init(): void {
    22                 add_action( 'init', array( __CLASS__, 'register_checkout_redirect_route' ) );
    23                 add_action( 'init', array( __CLASS__, 'register_sync_storefront_cart_route' ) );
    24                 add_action( 'init', array( __CLASS__, 'register_read_storefront_cart_route' ) );
     22                add_action( 'rest_api_init', array( __CLASS__, 'register_checkout_redirect_route' ) );
     23                add_action( 'rest_api_init', array( __CLASS__, 'register_sync_storefront_cart_route' ) );
     24                add_action( 'rest_api_init', array( __CLASS__, 'register_read_storefront_cart_route' ) );
    2525                add_action( 'template_redirect', array( __CLASS__, 'maybe_handle_checkout_redirect' ) );
    2626        }
  • agentmatica-mcp-connector/tags/0.7.2/includes/modules/woocommerce/class-catalog-search.php

    r3736423 r3763005  
    123123                if ( str_contains( $title, $normalized ) ) {
    124124                        return 90;
     125                }
     126
     127                $words = preg_split( '/\s+/', $normalized );
     128                $words = false === $words ? array() : array_values(
     129                        array_filter(
     130                                $words,
     131                                static function ( string $word ): bool {
     132                                        return strlen( $word ) >= 2;
     133                                }
     134                        )
     135                );
     136                if ( $words !== array() ) {
     137                        $title_hits = 0;
     138                        foreach ( $words as $word ) {
     139                                if ( self::word_matches_haystack( $word, $title ) ) {
     140                                        ++$title_hits;
     141                                }
     142                        }
     143                        if ( $title_hits === count( $words ) ) {
     144                                return 85;
     145                        }
    125146                }
    126147
  • agentmatica-mcp-connector/tags/0.7.2/readme.txt

    r3736588 r3763005  
    44Requires at least: 6.9
    55Tested up to: 7.1
    6 Requires PHP: 7.4
    7 Stable tag: 0.7.1
     6Requires PHP: 8.0
     7Stable tag: 0.7.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    9797== Changelog ==
    9898
     99= 0.7.2 =
     100* WooCommerce `search_catalog` no longer loads the entire catalogue on each chat search (SQL + capped re-rank). Fixes timeouts on large shops.
     101* `get_product` can resolve by product name when `id` is missing.
     102* Checkout and storefront-cart REST routes register on `rest_api_init`.
     103* An exact SKU search only returns published products.
     104* Requires PHP 8.0. Earlier versions already needed it to load.
     105
    99106= 0.7.1 =
    100107* Fixed: order attribution was sent to an address that does not exist, so sales made through the chat were not counted in Agentmatica Insights.
    … …  
    134141== Upgrade Notice ==
    135142
     143= 0.7.2 =
     144Woo catalog search no longer times out on large inventories. Update recommended for Agentmatica chat.
     145
    136146= 0.7.1 =
    137147Fixes order attribution so chat sales appear in Insights. Update recommended.
  • agentmatica-mcp-connector/trunk/agentmatica.php

    r3736588 r3763005  
    44 * Plugin URI: https://agentmatica.com/docs/integrations/wordpress
    55 * Description: Free MCP connector for Agentmatica.com. Expose WordPress content and optional WooCommerce tools to Agentmatica visitor chat.
    6  * Version: 0.7.1
     6 * Version: 0.7.2
    77 * Requires at least: 6.9
    8  * Requires PHP: 7.4
     8 * Requires PHP: 8.0
    99 * Author: Thought Bulb
    1010 * Author URI: https://thoughtbulb.dev
    … …  
    2323}
    2424
    25 define( 'AGENTMATICA_VERSION', '0.7.1' );
     25define( 'AGENTMATICA_VERSION', '0.7.2' );
    2626define( 'AGENTMATICA_PLUGIN_FILE', __FILE__ );
    2727define( 'AGENTMATICA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
  • agentmatica-mcp-connector/trunk/includes/class-ucp-catalog.php

    r3736423 r3763005  
    4444
    4545                if ( $query !== '' ) {
    46                         $products = self::query_published_products_by_search( $query, $limit );
     46                        $found    = self::query_published_products_by_search( $query, $limit );
     47                        $products = $found['products'];
     48                        $total    = $found['total'];
    4749                } else {
    48                         $products = self::query_all_published_products( $limit );
    49                 }
    50 
    51                 $total_count = self::count_catalog_products();
    52                 $payload     = array();
     50                        $found    = self::query_all_published_products( $limit );
     51                        $products = $found['products'];
     52                        $total    = $found['total'];
     53                }
     54
     55                $payload = array();
    5356
    5457                foreach ( $products as $product ) {
    … …  
    7376                }
    7477
    75                 return self::catalog_response( $payload, $total_count, $limit );
     78                return self::catalog_response( $payload, $total, $limit );
    7679        }
    7780
    … …  
    166169                        : $arguments;
    167170                $raw_id  = isset( $catalog['id'] ) ? (string) $catalog['id'] : '';
     171                if ( $raw_id === '' && isset( $arguments['id'] ) ) {
     172                        $raw_id = (string) $arguments['id'];
     173                }
    168174
    169175                $product = self::load_product_by_identifier( $raw_id );
    170176                if ( ! $product ) {
     177                        $name = '';
     178                        if ( isset( $catalog['name'] ) ) {
     179                                $name = sanitize_text_field( (string) $catalog['name'] );
     180                        } elseif ( isset( $arguments['name'] ) ) {
     181                                $name = sanitize_text_field( (string) $arguments['name'] );
     182                        } elseif ( isset( $catalog['query'] ) ) {
     183                                $name = sanitize_text_field( (string) $catalog['query'] );
     184                        }
     185
     186                        if ( $name !== '' ) {
     187                                return self::search_catalog(
     188                                        array(
     189                                                'query'      => $name,
     190                                                'pagination' => array( 'limit' => 5 ),
     191                                        )
     192                                );
     193                        }
     194
    171195                        return self::catalog_response( array() );
    172196                }
    … …  
    260284        }
    261285
    262         private static function count_catalog_products(): int {
    263                 if ( ! function_exists( 'wc_get_products' ) ) {
     286        /**
     287         * Published product count from post counts (no product hydration).
     288         */
     289        private static function count_published_products(): int {
     290                $counts = wp_count_posts( 'product' );
     291                if ( ! is_object( $counts ) || ! isset( $counts->publish ) ) {
    264292                        return 0;
    265293                }
    266294
    267                 $count = 0;
    268                 foreach ( self::load_published_product_objects( -1 ) as $product ) {
    269                         if ( self::is_catalog_available( $product ) && self::format_product( $product ) ) {
    270                                 ++$count;
    271                         }
    272                 }
    273 
    274                 return $count;
     295                return (int) $counts->publish;
    275296        }
    276297
    … …  
    308329         * Load published products for catalog browse (available to buy only).
    309330         *
    310          * @return list<WC_Product>
     331         * @return array{products: list<WC_Product>, total: int}
    311332         */
    312333        private static function query_all_published_products( int $limit ): array {
     334                $query = new WP_Query(
     335                        array(
     336                                'post_type'              => 'product',
     337                                'post_status'            => 'publish',
     338                                'posts_per_page'         => $limit,
     339                                'paged'                  => 1,
     340                                'orderby'                => 'date',
     341                                'order'                  => 'DESC',
     342                                'fields'                 => 'ids',
     343                                'no_found_rows'          => false,
     344                                'ignore_sticky_posts'    => true,
     345                                'update_post_meta_cache' => false,
     346                                'update_post_term_cache' => false,
     347                                'meta_query'             => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- instock browse only, paged.
     348                                        array(
     349                                                'key'   => '_stock_status',
     350                                                'value' => 'instock',
     351                                        ),
     352                                ),
     353                        )
     354                );
     355
    313356                $products = array();
    314                 foreach ( self::load_published_product_objects( -1 ) as $product ) {
     357                foreach ( $query->posts as $product_id ) {
     358                        $product = wc_get_product( (int) $product_id );
     359                        if ( ! $product instanceof WC_Product ) {
     360                                continue;
     361                        }
    315362                        if ( ! self::is_catalog_available( $product ) ) {
    316363                                continue;
    317364                        }
    318 
    319365                        $products[] = $product;
    320                         if ( count( $products ) >= $limit ) {
    321                                 break;
    322                         }
    323                 }
    324 
    325                 return $products;
    326         }
    327 
    328         /**
    329          * @return list<WC_Product>
    330          */
    331         private static function load_published_product_objects( int $limit ): array {
    332                 $ids = get_posts(
    333                         array(
    334                                 'post_type'      => 'product',
    335                                 'post_status'    => 'publish',
    336                                 'posts_per_page' => $limit,
    337                                 'orderby'        => 'date',
    338                                 'order'          => 'DESC',
    339                                 'fields'         => 'ids',
    340                         )
    341                 );
    342 
    343                 if ( ! is_array( $ids ) ) {
    344                         return array();
    345                 }
    346 
    347                 $products = array();
    348                 foreach ( $ids as $product_id ) {
    349                         $product = wc_get_product( (int) $product_id );
    350                         if ( $product instanceof WC_Product ) {
    351                                 $products[] = $product;
    352                         }
    353                 }
    354 
    355                 return $products;
    356         }
    357 
    358         /**
    359          * Named search — may include unavailable matches so the agent can explain stock status.
     366                }
     367
     368                $total = (int) $query->found_posts;
     369                if ( $total < 1 ) {
     370                        $total = self::count_published_products();
     371                }
     372
     373                return array(
     374                        'products' => $products,
     375                        'total'    => $total,
     376                );
     377        }
     378
     379        /**
     380         * Named search — SQL/Woo search plus a capped PHP re-rank. Never hydrates the full catalogue.
    360381         *
    361          * @return list<WC_Product>
     382         * @return array{products: list<WC_Product>, total: int}
    362383         */
    363384        private static function query_published_products_by_search( string $search, int $limit ): array {
    364385                $normalized = strtolower( trim( preg_replace( '/\s+/', ' ', $search ) ) );
    365386                if ( $normalized === '' ) {
    366                         return array();
    367                 }
     387                        return array(
     388                                'products' => array(),
     389                                'total'    => 0,
     390                        );
     391                }
     392
     393                $candidate_limit = min( 100, max( $limit * 5, 40 ) );
     394                $found           = self::search_published_product_ids( $normalized, $candidate_limit );
     395                $ids             = $found['ids'];
     396                $total           = $found['total'];
    368397
    369398                $scored = array();
    370 
    371                 foreach ( self::load_published_product_objects( -1 ) as $product ) {
     399                foreach ( $ids as $product_id ) {
     400                        $product = wc_get_product( $product_id );
     401                        if ( ! $product instanceof WC_Product ) {
     402                                continue;
     403                        }
     404
    372405                        $score = Agentmatica_Catalog_Search::product_match_score( $product, $search );
    373406                        if ( $score <= 0 ) {
    374                                 continue;
     407                                $score = 10;
    375408                        }
    376409
    377410                        $scored[] = array(
    378411                                'score'   => $score,
     412                                'id'      => $product_id,
    379413                                'product' => $product,
    380414                        );
    … …  
    384418                        $scored,
    385419                        static function ( array $left, array $right ): int {
    386                                 return $right['score'] <=> $left['score'];
     420                                $cmp = $right['score'] <=> $left['score'];
     421                                if ( 0 !== $cmp ) {
     422                                        return $cmp;
     423                                }
     424
     425                                return $left['id'] <=> $right['id'];
    387426                        }
    388427                );
    … …  
    396435                }
    397436
    398                 return $products;
     437                if ( $total < count( $products ) ) {
     438                        $total = count( $products );
     439                }
     440
     441                return array(
     442                        'products' => $products,
     443                        'total'    => $total,
     444                );
     445        }
     446
     447        /**
     448         * @return array{ids: list<int>, total: int}
     449         */
     450        private static function search_published_product_ids( string $search, int $limit ): array {
     451                $ids   = array();
     452                $total = 0;
     453
     454                $sku_id = function_exists( 'wc_get_product_id_by_sku' ) ? wc_get_product_id_by_sku( $search ) : 0;
     455                if ( $sku_id ) {
     456                        $sku_product = wc_get_product( $sku_id );
     457                        if ( $sku_product instanceof WC_Product_Variation ) {
     458                                $sku_id = (int) $sku_product->get_parent_id();
     459                        }
     460                        // wc_get_product_id_by_sku() matches drafts and private products too.
     461                        if ( $sku_id > 0 && get_post_status( $sku_id ) === 'publish' ) {
     462                                $ids[] = $sku_id;
     463                        }
     464                }
     465
     466                $query = new WP_Query(
     467                        array(
     468                                'post_type'              => 'product',
     469                                'post_status'            => 'publish',
     470                                'posts_per_page'         => $limit,
     471                                's'                      => $search,
     472                                'fields'                 => 'ids',
     473                                'no_found_rows'          => false,
     474                                'ignore_sticky_posts'    => true,
     475                                'update_post_meta_cache' => false,
     476                                'update_post_term_cache' => false,
     477                        )
     478                );
     479
     480                foreach ( $query->posts as $product_id ) {
     481                        $ids[] = (int) $product_id;
     482                }
     483                $total = max( $total, (int) $query->found_posts, count( $ids ) );
     484
     485                $title_found = self::search_product_ids_by_title_tokens( $search, $limit );
     486                $ids         = array_merge( $title_found['ids'], $ids );
     487                $total       = max( $total, $title_found['total'] );
     488
     489                $ids = array_values( array_unique( array_filter( array_map( 'intval', $ids ) ) ) );
     490
     491                return array(
     492                        'ids'   => $ids,
     493                        'total' => $total,
     494                );
     495        }
     496
     497        /**
     498         * Title AND-match for all query tokens (older fixtures WP `s=` ranking can drop).
     499         *
     500         * @return array{ids: list<int>, total: int}
     501         */
     502        private static function search_product_ids_by_title_tokens( string $search, int $limit ): array {
     503                global $wpdb;
     504
     505                $words  = preg_split( '/\s+/', $search );
     506                $tokens = array();
     507                foreach ( false === $words ? array() : $words as $word ) {
     508                        $word = trim( (string) $word );
     509                        if ( strlen( $word ) < 3 ) {
     510                                continue;
     511                        }
     512                        $tokens[] = $word;
     513                }
     514                $tokens = array_values( array_unique( $tokens ) );
     515                if ( $tokens === array() ) {
     516                        return array(
     517                                'ids'   => array(),
     518                                'total' => 0,
     519                        );
     520                }
     521
     522                $posts      = $wpdb->posts;
     523                $conditions = array( "post_type = 'product'", "post_status = 'publish'" );
     524                $values     = array();
     525                foreach ( $tokens as $token ) {
     526                        $conditions[] = 'post_title LIKE %s';
     527                        $values[]     = '%' . $wpdb->esc_like( $token ) . '%';
     528                }
     529
     530                $where    = implode( ' AND ', $conditions );
     531                $values[] = $limit;
     532
     533                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- `$posts` is `$wpdb->posts`; `$where` is LIKE %s placeholders.
     534                $ids = $wpdb->get_col(
     535                        $wpdb->prepare(
     536                                "SELECT ID FROM {$posts} WHERE {$where} ORDER BY ID ASC LIMIT %d",
     537                                ...$values
     538                        )
     539                );
     540
     541                $count_values = array_slice( $values, 0, -1 );
     542                $count        = $wpdb->get_var(
     543                        $wpdb->prepare(
     544                                "SELECT COUNT(ID) FROM {$posts} WHERE {$where}",
     545                                ...$count_values
     546                        )
     547                );
     548                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
     549
     550                return array(
     551                        'ids'   => array_map( 'intval', is_array( $ids ) ? $ids : array() ),
     552                        'total' => (int) $count,
     553                );
    399554        }
    400555
  • agentmatica-mcp-connector/trunk/includes/modules/woocommerce/class-cart.php

    r3736423 r3763005  
    2020
    2121        public static function init(): void {
    22                 add_action( 'init', array( __CLASS__, 'register_checkout_redirect_route' ) );
    23                 add_action( 'init', array( __CLASS__, 'register_sync_storefront_cart_route' ) );
    24                 add_action( 'init', array( __CLASS__, 'register_read_storefront_cart_route' ) );
     22                add_action( 'rest_api_init', array( __CLASS__, 'register_checkout_redirect_route' ) );
     23                add_action( 'rest_api_init', array( __CLASS__, 'register_sync_storefront_cart_route' ) );
     24                add_action( 'rest_api_init', array( __CLASS__, 'register_read_storefront_cart_route' ) );
    2525                add_action( 'template_redirect', array( __CLASS__, 'maybe_handle_checkout_redirect' ) );
    2626        }
  • agentmatica-mcp-connector/trunk/includes/modules/woocommerce/class-catalog-search.php

    r3736423 r3763005  
    123123                if ( str_contains( $title, $normalized ) ) {
    124124                        return 90;
     125                }
     126
     127                $words = preg_split( '/\s+/', $normalized );
     128                $words = false === $words ? array() : array_values(
     129                        array_filter(
     130                                $words,
     131                                static function ( string $word ): bool {
     132                                        return strlen( $word ) >= 2;
     133                                }
     134                        )
     135                );
     136                if ( $words !== array() ) {
     137                        $title_hits = 0;
     138                        foreach ( $words as $word ) {
     139                                if ( self::word_matches_haystack( $word, $title ) ) {
     140                                        ++$title_hits;
     141                                }
     142                        }
     143                        if ( $title_hits === count( $words ) ) {
     144                                return 85;
     145                        }
    125146                }
    126147
  • agentmatica-mcp-connector/trunk/readme.txt

    r3736588 r3763005  
    44Requires at least: 6.9
    55Tested up to: 7.1
    6 Requires PHP: 7.4
    7 Stable tag: 0.7.1
     6Requires PHP: 8.0
     7Stable tag: 0.7.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    9797== Changelog ==
    9898
     99= 0.7.2 =
     100* WooCommerce `search_catalog` no longer loads the entire catalogue on each chat search (SQL + capped re-rank). Fixes timeouts on large shops.
     101* `get_product` can resolve by product name when `id` is missing.
     102* Checkout and storefront-cart REST routes register on `rest_api_init`.
     103* An exact SKU search only returns published products.
     104* Requires PHP 8.0. Earlier versions already needed it to load.
     105
    99106= 0.7.1 =
    100107* Fixed: order attribution was sent to an address that does not exist, so sales made through the chat were not counted in Agentmatica Insights.
    … …  
    134141== Upgrade Notice ==
    135142
     143= 0.7.2 =
     144Woo catalog search no longer times out on large inventories. Update recommended for Agentmatica chat.
     145
    136146= 0.7.1 =
    137147Fixes order attribution so chat sales appear in Insights. Update recommended.
Note: See TracChangeset for help on using the changeset viewer.