| @@ -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( |