PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / trunk
MxChat – AI Chatbot & Content Generation for WordPress vtrunk
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | admin/class-pinecone-manager.php +99 -51 3.2.13trunk View file →
@@ -493,8 +493,12 @@
493 493 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
494 494
495 495 // Pinecone's list endpoint returns vector IDs with pagination
496 496 // We then fetch the metadata for those specific IDs
497 + // NOTE (plan 793b82): /vectors/list is a GET endpoint with query parameters.
498 + // A POST is answered 200-with-an-empty-body, which reads as "no vectors" —
499 + // this call used to POST and only ever "worked" because the empty result
500 + // tripped the query-based fallback below.
497 501 $list_url = "https://{$host}/vectors/list";
498 502
499 503 // Calculate pagination token from page number
500 504 // Pinecone uses cursor-based pagination, so we need to handle this differently
@@ -518,19 +522,20 @@
518 522 $list_params['paginationToken'] = $stored_tokens[$page];
519 523 }
520 524 }
521 525
522 - $response = wp_remote_post($list_url, array(
526 + $response = wp_remote_get($list_url . '?' . http_build_query($list_params), array(
523 527 'headers' => array(
524 528 'Api-Key' => $api_key,
525 - 'Content-Type' => 'application/json'
529 + 'accept' => 'application/json'
526 530 ),
527 - 'body' => json_encode($list_params),
528 531 'timeout' => 15
529 532 ));
530 533
531 534 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
532 - // Fallback to query-based approach
535 + // Genuine transport/API failure — fall back to the query-based approach,
536 + // but say so: a silent fallback is what masked the POST bug for a year.
537 + error_log('MxChat Pinecone: /vectors/list GET failed (' . (is_wp_error($response) ? $response->get_error_message() : wp_remote_retrieve_response_code($response)) . ') — falling back to query-based listing.');
533 538 return $this->mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type);
534 539 }
535 540
536 541 $body = wp_remote_retrieve_body($response);
@@ -549,15 +554,26 @@
549 554 $vector_ids[] = $vector['id'];
550 555 }
551 556 }
552 557
553 - // If list endpoint returned empty, fall back to query-based approach
558 + // A 200 with no vector ids is a REAL answer now (the namespace is empty),
559 + // not the failure signature it was under POST — return the empty listing
560 + // rather than routing into the fallback and masking a broken primary.
554 561 if (empty($vector_ids)) {
562 + return array('data' => array(), 'total' => 0);
563 + }
564 +
565 + // Fetch metadata for these vector IDs
566 + $fetched = $this->mxchat_fetch_vectors_by_ids_for_list($pinecone_options, $vector_ids, $page, $per_page, $bot_id, $content_type);
567 +
568 + // null = the fetch itself failed in transport (distinct from "ids had no
569 + // metadata matches") — log and fall back so the screen still lists.
570 + if ($fetched === null) {
571 + error_log('MxChat Pinecone: /vectors/fetch GET failed for the listing — falling back to query-based listing.');
555 572 return $this->mxchat_query_based_list($pinecone_options, $page, $per_page, $bot_id, $content_type);
556 573 }
557 574
558 - // Fetch metadata for these vector IDs
559 - return $this->mxchat_fetch_vectors_by_ids_for_list($pinecone_options, $vector_ids, $page, $per_page, $bot_id, $content_type);
575 + return $fetched;
560 576 }
561 577
562 578 /**
563 579 * Query-based listing fallback when list endpoint fails
@@ -661,29 +677,31 @@
661 677 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
662 678 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
663 679 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
664 680
665 - $fetch_url = "https://{$host}/vectors/fetch";
666 -
667 - $fetch_data = array(
668 - 'ids' => $vector_ids
669 - );
670 -
681 + // NOTE (plan 793b82): /vectors/fetch is a GET endpoint, and Pinecone expects
682 + // the ids repeated (ids=a&ids=b) — http_build_query would emit ids[0]=a.
683 + // A POST here is answered 200-with-an-empty-body, which reads as "no vectors".
684 + $fetch_query = array();
685 + foreach ($vector_ids as $fetch_vid) {
686 + $fetch_query[] = 'ids=' . rawurlencode($fetch_vid);
687 + }
671 688 if (!empty($namespace)) {
672 - $fetch_data['namespace'] = $namespace;
689 + $fetch_query[] = 'namespace=' . rawurlencode($namespace);
673 690 }
674 691
675 - $response = wp_remote_post($fetch_url, array(
692 + $response = wp_remote_get("https://{$host}/vectors/fetch?" . implode('&', $fetch_query), array(
676 693 'headers' => array(
677 694 'Api-Key' => $api_key,
678 - 'Content-Type' => 'application/json'
695 + 'accept' => 'application/json'
679 696 ),
680 - 'body' => json_encode($fetch_data),
681 697 'timeout' => 15
682 698 ));
683 699
700 + // Transport/API failure is distinct from "nothing matched": return null so
701 + // the caller can tell the difference (plan 793b82's empty-vs-failed rule).
684 702 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
685 - return array('data' => array(), 'total' => 0);
703 + return null;
686 704 }
687 705
688 706 $body = wp_remote_retrieve_body($response);
689 707 $data = json_decode($body, true);
@@ -1104,31 +1122,45 @@
1104 1122 if (empty($vector_id_map)) {
1105 1123 return array();
1106 1124 }
1107 1125
1108 - // Batch check Pinecone using fetch API (max 1000 IDs per request)
1126 + // Batch check Pinecone using fetch API.
1127 + // NOTE (plan 793b82): /vectors/fetch is a GET endpoint with the ids
1128 + // repeated in the query string (ids=a&ids=b); the old POST here was
1129 + // answered 200-with-an-empty-body, so this scan saw NOTHING as indexed
1130 + // and callers re-embedded content Pinecone already had. Chunk size 100:
1131 + // measured on a live serverless index, ~9KB of URL is accepted and
1132 + // ~18KB draws HTTP 414, so 100 32-char ids (~4KB) leaves real margin.
1133 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1109 1134 $all_vector_ids = array_keys($vector_id_map);
1110 - $chunks = array_chunk($all_vector_ids, 1000);
1135 + $chunks = array_chunk($all_vector_ids, 100);
1111 1136 $processed_data = array();
1112 1137
1113 1138 foreach ($chunks as $chunk) {
1114 - $fetch_url = "https://{$host}/vectors/fetch";
1139 + $fetch_query = array();
1140 + foreach ($chunk as $fetch_vid) {
1141 + $fetch_query[] = 'ids=' . rawurlencode($fetch_vid);
1142 + }
1143 + if (!empty($namespace)) {
1144 + $fetch_query[] = 'namespace=' . rawurlencode($namespace);
1145 + }
1115 1146
1116 - $response = wp_remote_post($fetch_url, array(
1147 + $response = wp_remote_get("https://{$host}/vectors/fetch?" . implode('&', $fetch_query), array(
1117 1148 'headers' => array(
1118 1149 'Api-Key' => $api_key,
1119 - 'Content-Type' => 'application/json'
1150 + 'accept' => 'application/json'
1120 1151 ),
1121 - 'body' => json_encode(array('ids' => $chunk)),
1122 1152 'timeout' => 30
1123 1153 ));
1124 1154
1125 1155 if (is_wp_error($response)) {
1156 + error_log('MxChat Pinecone: processed-content scan /vectors/fetch GET failed: ' . $response->get_error_message());
1126 1157 continue;
1127 1158 }
1128 1159
1129 1160 $response_code = wp_remote_retrieve_response_code($response);
1130 1161 if ($response_code !== 200) {
1162 + error_log('MxChat Pinecone: processed-content scan /vectors/fetch GET returned HTTP ' . $response_code);
1131 1163 continue;
1132 1164 }
1133 1165
1134 1166 $body = wp_remote_retrieve_body($response);
@@ -1311,8 +1343,9 @@
1311 1343 */
1312 1344 public function fetch_pinecone_vectors_by_ids($pinecone_options, $vector_ids) {
1313 1345 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1314 1346 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1347 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1315 1348
1316 1349 if (empty($api_key) || empty($host) || empty($vector_ids)) {
1317 1350 return array();
1318 1351 }
@@ -1317,44 +1350,53 @@
1317 1350 return array();
1318 1351 }
1319 1352
1320 1353 try {
1321 - $fetch_url = "https://{$host}/vectors/fetch";
1354 + // NOTE (plan 793b82): /vectors/fetch is a GET endpoint with the ids
1355 + // repeated in the query string; the old POST here was answered
1356 + // 200-with-an-empty-body. Chunked at 100 ids to stay well under the
1357 + // measured HTTP 414 URL-length boundary.
1358 + $vectors = array();
1359 + foreach (array_chunk(array_values($vector_ids), 100) as $chunk) {
1360 + $fetch_query = array();
1361 + foreach ($chunk as $fetch_vid) {
1362 + $fetch_query[] = 'ids=' . rawurlencode($fetch_vid);
1363 + }
1364 + if (!empty($namespace)) {
1365 + $fetch_query[] = 'namespace=' . rawurlencode($namespace);
1366 + }
1322 1367
1323 - // Pinecone fetch API allows fetching specific vectors by ID
1324 - $fetch_data = array(
1325 - 'ids' => array_values($vector_ids)
1326 - );
1368 + $response = wp_remote_get("https://{$host}/vectors/fetch?" . implode('&', $fetch_query), array(
1369 + 'headers' => array(
1370 + 'Api-Key' => $api_key,
1371 + 'accept' => 'application/json'
1372 + ),
1373 + 'timeout' => 30
1374 + ));
1327 1375
1328 - $response = wp_remote_post($fetch_url, array(
1329 - 'headers' => array(
1330 - 'Api-Key' => $api_key,
1331 - 'Content-Type' => 'application/json'
1332 - ),
1333 - 'body' => json_encode($fetch_data),
1334 - 'timeout' => 30
1335 - ));
1376 + if (is_wp_error($response)) {
1377 + error_log('MxChat Pinecone: fetch_pinecone_vectors_by_ids GET failed: ' . $response->get_error_message());
1378 + continue;
1379 + }
1336 1380
1337 - if (is_wp_error($response)) {
1338 - return array();
1339 - }
1381 + if (wp_remote_retrieve_response_code($response) !== 200) {
1382 + error_log('MxChat Pinecone: fetch_pinecone_vectors_by_ids GET returned HTTP ' . wp_remote_retrieve_response_code($response));
1383 + continue;
1384 + }
1340 1385
1341 - $response_code = wp_remote_retrieve_response_code($response);
1342 -
1343 - if ($response_code !== 200) {
1344 - return array();
1386 + $data = json_decode(wp_remote_retrieve_body($response), true);
1387 + if (isset($data['vectors']) && is_array($data['vectors'])) {
1388 + $vectors += $data['vectors'];
1389 + }
1345 1390 }
1346 1391
1347 - $body = wp_remote_retrieve_body($response);
1348 - $data = json_decode($body, true);
1349 -
1350 - if (!isset($data['vectors'])) {
1392 + if (empty($vectors)) {
1351 1393 return array();
1352 1394 }
1353 1395
1354 1396 $processed_data = array();
1355 1397
1356 - foreach ($data['vectors'] as $vector_id => $vector_data) {
1398 + foreach ($vectors as $vector_id => $vector_data) {
1357 1399 $metadata = $vector_data['metadata'] ?? array();
1358 1400 $source_url = $metadata['source_url'] ?? '';
1359 1401
1360 1402 if (!empty($source_url)) {
@@ -1397,8 +1439,9 @@
1397 1439 */
1398 1440 public function mxchat_delete_all_from_pinecone($pinecone_options, $content_type_filter = '') {
1399 1441 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
1400 1442 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
1443 + $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
1401 1444
1402 1445 if (empty($api_key) || empty($host)) {
1403 1446 return array(
1404 1447 'success' => false,
@@ -1443,9 +1486,9 @@
1443 1486 $batch_size = 100;
1444 1487 $batches = array_chunk($vector_ids, $batch_size);
1445 1488
1446 1489 foreach ($batches as $batch) {
1447 - $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host);
1490 + $result = $this->mxchat_delete_pinecone_batch($batch, $api_key, $host, $namespace);
1448 1491 if ($result['success']) {
1449 1492 $total_deleted += count($batch);
1450 1493 } else {
1451 1494 $failed_batches++;
@@ -1486,9 +1529,9 @@
1486 1529
1487 1530 /**
1488 1531 * Deletes batch of vectors from Pinecone database
1489 1532 */
1490 - public function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host) {
1533 + public function mxchat_delete_pinecone_batch($vector_ids, $api_key, $host, $namespace = '') {
1491 1534 // Build the API endpoint
1492 1535 $api_endpoint = "https://{$host}/vectors/delete";
1493 1536
1494 1537 // Prepare the request body with the IDs
@@ -1494,8 +1537,13 @@
1494 1537 // Prepare the request body with the IDs
1495 1538 $request_body = array(
1496 1539 'ids' => $vector_ids
1497 1540 );
1541 +
1542 + // Add namespace if provided — omitting it targets the default namespace
1543 + if (!empty($namespace)) {
1544 + $request_body['namespace'] = $namespace;
1545 + }
1498 1546
1499 1547 // Make the deletion request
1500 1548 $response = wp_remote_post($api_endpoint, array(
1501 1549 'headers' => array(