= $len) break; $start += ($size - $overlap); } return $chunks; } function uuid(): string { $b = random_bytes(16); $b[6] = chr((ord($b[6]) & 0x0f) | 0x40); $b[8] = chr((ord($b[8]) & 0x3f) | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4)); } logln('=== Ingest Guida UI V2 nella KB (scope SYSTEM) ==='); if ($dryRun) logln('MODALITA DRY-RUN: nessun upsert.'); if (!is_file($DOC_FILE)) { logln("ERRORE: doc non trovato {$DOC_FILE}"); exit(1); } $text = (string) file_get_contents($DOC_FILE); $text = preg_replace('/[ \t]+/', ' ', $text); $text = preg_replace('/\n{3,}/', "\n\n", trim($text)); if (strlen($text) < 200) { logln('ERRORE: testo troppo breve.'); exit(1); } $header = "DOCUMENTO: Guida all'interfaccia V2 di NIS2 Agile (Bootstrap Italia).\n" . "USO: aiutare l'utente a navigare la UI, trovare moduli e funzioni.\n\n"; $chunks = chunkText($header . $text, 2000, 200); logln(' testo: ' . strlen($text) . ' char -> ' . count($chunks) . ' chunk'); if ($dryRun) { logln('=== DRY-RUN completato ==='); exit(0); } $embed = new EmbedService(); $vector = new VectorService(); $vector->ensureCollection($embed->dims); // Idempotenza: rimuovi i chunk SYSTEM esistenti per questa fonte try { $vector->deleteByFilter(['must' => [ ['key' => 'scope', 'match' => ['value' => 'SYSTEM']], ['key' => 'source', 'match' => ['value' => $SOURCE]], ]]); } catch (Exception $e) { logln(' (warning) delete precedente: ' . $e->getMessage()); } $docUuid = uuid(); $points = []; foreach ($chunks as $i => $chunk) { $vec = null; for ($try = 1; $try <= 5; $try++) { try { $vec = $embed->embed($chunk); break; } catch (Throwable $e) { if ($try === 5) { logln(" ERRORE embed chunk {$i} dopo 5 tentativi: " . $e->getMessage()); throw $e; } logln(" retry embed chunk {$i} (tentativo {$try})"); sleep($try); } } $points[] = [ 'id' => uuid(), 'vector' => $vec, 'payload' => [ 'doc_uuid' => $docUuid, 'title' => $TITLE . ($i > 0 ? ' (parte ' . ($i + 1) . ')' : ''), 'chunk' => $chunk, 'entity_type' => 'guida_prodotto', 'source' => $SOURCE, 'lang' => 'it', 'scope' => 'SYSTEM', 'consulting_firm_id' => null, 'organization_id' => null, 'shared_with_orgs' => [], 'uploaded_by' => 0, ], ]; } foreach (array_chunk($points, 64) as $batch) { $vector->upsertBatch($batch); } // Tracking MySQL (best-effort) try { $stmt = Database::getInstance()->prepare( "INSERT INTO kb_uploaded_documents (qdrant_doc_uuid, scope, consulting_firm_id, organization_id, uploaded_by, title, entity_type, source, lang, chunk_count, shared_with_orgs, status) VALUES (?, 'SYSTEM', NULL, NULL, 0, ?, 'guida_prodotto', ?, 'it', ?, '[]', 'ready')" ); $stmt->execute([$docUuid, $TITLE, $SOURCE, count($chunks)]); } catch (Exception $e) { logln(' (warning) tracking insert: ' . $e->getMessage()); } logln("=== OK indicizzato: doc_uuid={$docUuid}, " . count($chunks) . " chunk ===");