BxDolAIVectorStoreMariaDb.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. <?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * MariaDB vector store on the site database connection.
  7. * Embeddings of width 1024 and 384 live in separate tables, created from install/sql/mariadb.sql.
  8. *
  9. * @defgroup UnaCore UNA Core
  10. * @{
  11. */
  12. class BxDolAIVectorStoreMariaDb
  13. {
  14. public const MARIADB_MIN_VERSION = '11.7.0';
  15. public const DIMENSIONS = [1024, 384];
  16. /**
  17. * @return array{engine:string, version:string, supported:bool}
  18. */
  19. public static function describe(string $sVersion): array
  20. {
  21. $sVersion = trim($sVersion);
  22. if ($sVersion === '')
  23. return ['engine' => '', 'version' => '', 'supported' => false];
  24. $bMaria = stripos($sVersion, 'mariadb') !== false;
  25. $sEngine = $bMaria ? 'mariadb' : 'mysql';
  26. $sNumber = self::versionNumber($sVersion, $bMaria);
  27. $bSupported = $bMaria && $sNumber !== '' && self::atLeast($sNumber, self::MARIADB_MIN_VERSION);
  28. return [
  29. 'engine' => $sEngine,
  30. 'version' => $sNumber,
  31. 'supported' => $bSupported,
  32. ];
  33. }
  34. public static function compatibilityMessage(array $aServer): string
  35. {
  36. if (($aServer['engine'] ?? '') === 'mariadb')
  37. $sEngine = 'MariaDB';
  38. elseif (($aServer['engine'] ?? '') === 'mysql')
  39. $sEngine = 'MySQL';
  40. else
  41. $sEngine = 'unknown';
  42. $sVersion = (string)($aServer['version'] ?? '');
  43. if ($sVersion === '')
  44. $sVersion = 'unknown';
  45. return 'MariaDB vector store needs MariaDB ' . self::MARIADB_MIN_VERSION . ' or newer (connected: ' . $sEngine . ' ' . $sVersion . ')';
  46. }
  47. public static function tableName(string $sTable): string
  48. {
  49. if (!preg_match('/^[A-Za-z0-9_]+$/', $sTable))
  50. throw new Exception('MariaDB vector store table name is invalid');
  51. return $sTable;
  52. }
  53. public static function tableForDimension(int $iDimension): string
  54. {
  55. if (!in_array($iDimension, self::DIMENSIONS, true))
  56. throw new Exception('MariaDB vector store expects embedding width 1024 or 384, got ' . $iDimension);
  57. return self::tableName('sys_agents_vector_mariadb_' . $iDimension);
  58. }
  59. public static function pdo(): PDO
  60. {
  61. BxDolDb::getInstance();
  62. $oPdo = BxDolDb::getLink();
  63. if (!$oPdo instanceof PDO)
  64. throw new Exception('Database is not connected');
  65. return $oPdo;
  66. }
  67. public static function open(int $iStoreId, int $iTopK): BxDolAIVectorStoreMariaDbStore
  68. {
  69. if ($iStoreId < 1)
  70. throw new Exception('MariaDB vector store id is missing');
  71. if ($iTopK < 1)
  72. $iTopK = 4;
  73. $oDb = BxDolDb::getInstance();
  74. $sVersion = $oDb ? (string)$oDb->getOne('SELECT VERSION()') : '';
  75. $aServer = self::describe($sVersion);
  76. if (empty($aServer['supported'])) {
  77. $sMessage = self::compatibilityMessage($aServer);
  78. bx_log('sys_agents', $sMessage, BX_LOG_ERR);
  79. throw new Exception($sMessage);
  80. }
  81. foreach (self::DIMENSIONS as $iDimension)
  82. self::assertTable($iDimension);
  83. return new BxDolAIVectorStoreMariaDbStore(self::pdo(), $iTopK, (string)$iStoreId);
  84. }
  85. protected static function versionNumber(string $sVersion, bool $bMaria): string
  86. {
  87. if ($bMaria && preg_match('/(\d+\.\d+(?:\.\d+)?)-MariaDB/i', $sVersion, $aMatch))
  88. return $aMatch[1];
  89. if (preg_match('/(\d+\.\d+(?:\.\d+)?)/', $sVersion, $aMatch))
  90. return $aMatch[1];
  91. return '';
  92. }
  93. protected static function atLeast(string $sVersion, string $sMinimum): bool
  94. {
  95. return version_compare(self::normalizeVersion($sVersion), self::normalizeVersion($sMinimum), '>=');
  96. }
  97. protected static function normalizeVersion(string $sVersion): string
  98. {
  99. $aParts = explode('.', $sVersion);
  100. $aParts = array_slice(array_pad($aParts, 3, '0'), 0, 3);
  101. return implode('.', $aParts);
  102. }
  103. protected static function assertTable(int $iDimension): void
  104. {
  105. $sTable = self::tableForDimension($iDimension);
  106. $iExisting = self::embeddingDimensions($sTable);
  107. if ($iExisting === null)
  108. throw new Exception('MariaDB vector store table ' . $sTable . ' is missing');
  109. if ($iExisting !== $iDimension)
  110. throw new Exception('MariaDB vector store ' . $sTable . ' must use dimension ' . $iDimension);
  111. }
  112. protected static function embeddingDimensions(string $sTable): ?int
  113. {
  114. $oDb = BxDolDb::getInstance();
  115. if (!$oDb)
  116. return null;
  117. $sType = $oDb->getOne(
  118. 'SELECT `COLUMN_TYPE` FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = ? AND `COLUMN_NAME` = ?',
  119. [$sTable, 'embedding']
  120. );
  121. if (!is_string($sType) || !preg_match('/vector\((\d+)\)/i', $sType, $aMatch))
  122. return null;
  123. return (int)$aMatch[1];
  124. }
  125. }
  126. /** @} */