BxDolAIVectorStoreMariaDbTest.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. use PHPUnit\Framework\Attributes\DataProvider;
  3. class BxDolAIVectorStoreMariaDbTest extends BxDolTestCase
  4. {
  5. #[DataProvider('providerForDescribe')]
  6. public function testDescribe(string $sVersion, string $sEngine, string $sNumber, bool $bSupported)
  7. {
  8. $a = BxDolAIVectorStoreMariaDb::describe($sVersion);
  9. $this->assertSame($sEngine, $a['engine']);
  10. $this->assertSame($sNumber, $a['version']);
  11. $this->assertSame($bSupported, $a['supported']);
  12. }
  13. public static function providerForDescribe(): array
  14. {
  15. return [
  16. ['11.8.2-MariaDB-ubu2404', 'mariadb', '11.8.2', true],
  17. ['12.1.2-MariaDB-ubu2404', 'mariadb', '12.1.2', true],
  18. ['11.7.0-MariaDB', 'mariadb', '11.7.0', true],
  19. ['11.7-MariaDB', 'mariadb', '11.7', true],
  20. ['11.6.2-MariaDB', 'mariadb', '11.6.2', false],
  21. ['10.11.6-MariaDB-1:10.11.6+maria~ubu2204', 'mariadb', '10.11.6', false],
  22. ['5.5.5-10.11.6-MariaDB', 'mariadb', '10.11.6', false],
  23. ['5.5.5-11.7.2-MariaDB', 'mariadb', '11.7.2', true],
  24. ['8.0.36', 'mysql', '8.0.36', false],
  25. ['8.0.36-0ubuntu0.22.04.1', 'mysql', '8.0.36', false],
  26. ['8.4.3', 'mysql', '8.4.3', false],
  27. ['9.0.0', 'mysql', '9.0.0', false],
  28. ['9.2.0-commercial', 'mysql', '9.2.0', false],
  29. ['', '', '', false],
  30. ];
  31. }
  32. public function testCompatibilityMessageNamesEngineAndVersion()
  33. {
  34. $sMaria = BxDolAIVectorStoreMariaDb::compatibilityMessage(
  35. BxDolAIVectorStoreMariaDb::describe('10.11.6-MariaDB')
  36. );
  37. $sMysql = BxDolAIVectorStoreMariaDb::compatibilityMessage(
  38. BxDolAIVectorStoreMariaDb::describe('9.0.0')
  39. );
  40. $this->assertStringContainsString('MariaDB 11.7.0', $sMaria);
  41. $this->assertStringContainsString('MariaDB 10.11.6', $sMaria);
  42. $this->assertStringContainsString('MySQL 9.0.0', $sMysql);
  43. }
  44. public function testTableName()
  45. {
  46. $this->assertSame('sys_agents_vector_mariadb_1024', BxDolAIVectorStoreMariaDb::tableForDimension(1024));
  47. $this->assertSame('sys_agents_vector_mariadb_384', BxDolAIVectorStoreMariaDb::tableForDimension(384));
  48. try {
  49. BxDolAIVectorStoreMariaDb::tableForDimension(1536);
  50. $this->fail('An embedding width must be 1024 or 384');
  51. }
  52. catch (Exception $oException) {
  53. $this->assertSame('MariaDB vector store expects embedding width 1024 or 384, got 1536', $oException->getMessage());
  54. }
  55. try {
  56. BxDolAIVectorStoreMariaDb::tableName('sys_agents_vs_1; DROP TABLE sys_accounts');
  57. $this->fail('A table name must be an identifier');
  58. }
  59. catch (Exception $oException) {
  60. $this->assertSame('MariaDB vector store table name is invalid', $oException->getMessage());
  61. }
  62. }
  63. public function testPdoIsBxDolDbLink()
  64. {
  65. BxDolDb::getInstance();
  66. $this->assertSame(BxDolDb::getLink(), BxDolAIVectorStoreMariaDb::pdo());
  67. }
  68. public function testWrongEmbeddingWidthIsRejected()
  69. {
  70. $this->skipUnlessMariaDb();
  71. $oStore = BxDolAIVectorStoreMariaDb::open(903, 4);
  72. $oDocument = new NeuronAI\RAG\Document('width-mismatch');
  73. $oDocument->embedding = [0.1, 0.2, 0.3];
  74. $oDocument->sourceType = '903';
  75. $oDocument->sourceName = 'width-mismatch';
  76. try {
  77. $oStore->addDocument($oDocument);
  78. $this->fail('A short embedding must not be stored');
  79. }
  80. catch (Exception $oException) {
  81. $this->assertSame(
  82. 'MariaDB vector store expects embedding width 1024 or 384, got 3',
  83. $oException->getMessage()
  84. );
  85. }
  86. foreach (BxDolAIVectorStoreMariaDb::DIMENSIONS as $iDimension) {
  87. $sTable = BxDolAIVectorStoreMariaDb::tableForDimension($iDimension);
  88. $iLeft = (int)BxDolDb::getInstance()->getOne(
  89. "SELECT COUNT(*) FROM `{$sTable}` WHERE `sourceName` = 'width-mismatch'"
  90. );
  91. $this->assertSame(0, $iLeft, $sTable);
  92. }
  93. }
  94. public static function providerForDimension(): array
  95. {
  96. return [
  97. '1024' => [1024],
  98. '384' => [384],
  99. ];
  100. }
  101. #[DataProvider('providerForDimension')]
  102. public function testClosestDocumentRanksFirst(int $iDimension)
  103. {
  104. $this->skipUnlessMariaDb();
  105. $aNear = array_fill(0, $iDimension, 0.0);
  106. $aNear[0] = 1.0;
  107. $aFar = array_fill(0, $iDimension, 0.0);
  108. $aFar[1] = 1.0;
  109. $oStore = BxDolAIVectorStoreMariaDb::open(904, 4);
  110. $oFar = new NeuronAI\RAG\Document('far-chunk');
  111. $oFar->embedding = $aFar;
  112. $oFar->sourceType = '904';
  113. $oFar->sourceName = 'rank';
  114. $oNear = new NeuronAI\RAG\Document('near-chunk');
  115. $oNear->embedding = $aNear;
  116. $oNear->sourceType = '904';
  117. $oNear->sourceName = 'rank';
  118. try {
  119. $oStore->addDocument($oFar);
  120. $oStore->addDocument($oNear);
  121. $aFound = iterator_to_array($oStore->similaritySearch($aNear));
  122. $this->assertSame('near-chunk', $aFound[0]->getContent());
  123. $this->assertGreaterThan(0.9, $aFound[0]->getScore());
  124. $this->assertGreaterThanOrEqual(0.0, $aFound[1]->getScore());
  125. $this->assertGreaterThan($aFound[1]->getScore(), $aFound[0]->getScore());
  126. }
  127. finally {
  128. $oStore->deleteBy('904', 'rank');
  129. }
  130. }
  131. public function testSearchStaysOnTheMatchingDimensionTable()
  132. {
  133. $this->skipUnlessMariaDb();
  134. $aWide = array_fill(0, 1024, 0.0);
  135. $aWide[0] = 1.0;
  136. $aNarrow = array_fill(0, 384, 0.0);
  137. $aNarrow[0] = 1.0;
  138. $oStore = BxDolAIVectorStoreMariaDb::open(905, 4);
  139. $oWide = new NeuronAI\RAG\Document('wide-chunk');
  140. $oWide->embedding = $aWide;
  141. $oWide->sourceType = '905';
  142. $oWide->sourceName = 'cross-dim';
  143. $oNarrow = new NeuronAI\RAG\Document('narrow-chunk');
  144. $oNarrow->embedding = $aNarrow;
  145. $oNarrow->sourceType = '905';
  146. $oNarrow->sourceName = 'cross-dim';
  147. try {
  148. $oStore->addDocument($oWide);
  149. $oStore->addDocument($oNarrow);
  150. $aFromWide = iterator_to_array($oStore->similaritySearch($aWide));
  151. $aFromNarrow = iterator_to_array($oStore->similaritySearch($aNarrow));
  152. $this->assertCount(1, $aFromWide);
  153. $this->assertSame('wide-chunk', $aFromWide[0]->getContent());
  154. $this->assertCount(1, $aFromNarrow);
  155. $this->assertSame('narrow-chunk', $aFromNarrow[0]->getContent());
  156. $this->assertSame(1, $this->countBySourceName(1024, 'cross-dim'));
  157. $this->assertSame(1, $this->countBySourceName(384, 'cross-dim'));
  158. }
  159. finally {
  160. $oStore->deleteBy('905', 'cross-dim');
  161. }
  162. $this->assertSame(0, $this->countBySourceName(1024, 'cross-dim'));
  163. $this->assertSame(0, $this->countBySourceName(384, 'cross-dim'));
  164. }
  165. #[DataProvider('providerForDimension')]
  166. public function testSearchIsLimitedToStore(int $iDimension)
  167. {
  168. $this->skipUnlessMariaDb();
  169. $aNearA = array_fill(0, $iDimension, 0.0);
  170. $aNearA[0] = 1.0;
  171. $aNearB = array_fill(0, $iDimension, 0.0);
  172. $aNearB[1] = 1.0;
  173. $oStoreA = BxDolAIVectorStoreMariaDb::open(901, 4);
  174. $oStoreB = BxDolAIVectorStoreMariaDb::open(902, 4);
  175. $oDocA = new NeuronAI\RAG\Document('store-a-only');
  176. $oDocA->embedding = $aNearA;
  177. $oDocA->sourceType = '901';
  178. $oDocA->sourceName = 'search-filter';
  179. $oDocB = new NeuronAI\RAG\Document('store-b-only');
  180. $oDocB->embedding = $aNearB;
  181. $oDocB->sourceType = '902';
  182. $oDocB->sourceName = 'search-filter';
  183. try {
  184. $oStoreA->addDocument($oDocA);
  185. $oStoreB->addDocument($oDocB);
  186. $aFromA = iterator_to_array($oStoreA->similaritySearch($aNearB));
  187. $aFromB = iterator_to_array($oStoreB->similaritySearch($aNearB));
  188. $this->assertSame('store-a-only', $aFromA[0]->getContent());
  189. $this->assertSame('901', (string)$aFromA[0]->getSourceType());
  190. $this->assertSame('store-b-only', $aFromB[0]->getContent());
  191. $this->assertSame('902', (string)$aFromB[0]->getSourceType());
  192. }
  193. finally {
  194. $oStoreA->deleteBy('901', 'search-filter');
  195. $oStoreB->deleteBy('902', 'search-filter');
  196. }
  197. }
  198. public function testInstalledServerVersionIsRecognized()
  199. {
  200. $sVersion = (string)BxDolDb::getInstance()->getOne('SELECT VERSION()');
  201. $a = BxDolAIVectorStoreMariaDb::describe($sVersion);
  202. $this->assertContains($a['engine'], ['mariadb', 'mysql', '']);
  203. if ($a['version'] !== '')
  204. $this->assertMatchesRegularExpression('/^\d+\.\d+/', $a['version']);
  205. $bSupported = $a['engine'] === 'mariadb' && $a['version'] !== ''
  206. && version_compare(self::normalize($a['version']), BxDolAIVectorStoreMariaDb::MARIADB_MIN_VERSION, '>=');
  207. $this->assertSame($bSupported, $a['supported']);
  208. }
  209. protected function skipUnlessMariaDb(): void
  210. {
  211. $sVersion = (string)BxDolDb::getInstance()->getOne('SELECT VERSION()');
  212. if (!BxDolAIVectorStoreMariaDb::describe($sVersion)['supported'])
  213. $this->markTestSkipped('MariaDB vector store is not supported on this server');
  214. }
  215. protected function countBySourceName(int $iDimension, string $sSourceName): int
  216. {
  217. $sTable = BxDolAIVectorStoreMariaDb::tableForDimension($iDimension);
  218. return (int)BxDolDb::getInstance()->getOne(
  219. "SELECT COUNT(*) FROM `{$sTable}` WHERE `sourceName` = ?",
  220. [$sSourceName]
  221. );
  222. }
  223. protected static function normalize(string $sVersion): string
  224. {
  225. $aParts = explode('.', $sVersion);
  226. $aParts = array_slice(array_pad($aParts, 3, '0'), 0, 3);
  227. return implode('.', $aParts);
  228. }
  229. }