1f2bbffb5SAndreas Gohr<?php 2f2bbffb5SAndreas Gohr 3f2bbffb5SAndreas Gohrnamespace dokuwiki\Search\Collection; 4f2bbffb5SAndreas Gohr 5f2bbffb5SAndreas Gohruse dokuwiki\Search\Exception\IndexAccessException; 6f2bbffb5SAndreas Gohruse dokuwiki\Search\Exception\IndexLockException; 795b16223SAndreas Gohruse dokuwiki\Search\Exception\IndexUsageException; 8f2bbffb5SAndreas Gohruse dokuwiki\Search\Exception\IndexWriteException; 995b16223SAndreas Gohruse dokuwiki\Search\Index\AbstractIndex; 10f2bbffb5SAndreas Gohruse dokuwiki\Search\Index\FileIndex; 11f2bbffb5SAndreas Gohruse dokuwiki\Search\Index\Lock; 12f2bbffb5SAndreas Gohruse dokuwiki\Search\Index\MemoryIndex; 130a9fafedSAndreas Gohruse dokuwiki\Search\Index\TupleOps; 140a9fafedSAndreas Gohruse dokuwiki\Search\Tokenizer; 15f2bbffb5SAndreas Gohr 16f2bbffb5SAndreas Gohr/** 17f2bbffb5SAndreas Gohr * Abstract base class for index collections 18f2bbffb5SAndreas Gohr * 19f2bbffb5SAndreas Gohr * A collection manages a group of related indexes that together provide a specific search use case. 20f2bbffb5SAndreas Gohr * Every collection works with four index types: entity, token, frequency, and reverse. 21f2bbffb5SAndreas Gohr * 22f2bbffb5SAndreas Gohr * entity - the list of the main entities (eg. pages) 23f2bbffb5SAndreas Gohr * token - the list of tokens (eg. words) assigned to entities (can be split into multiple files) 24f2bbffb5SAndreas Gohr * frequency - how often a token appears on a entity (can be split into multiple files) 25f2bbffb5SAndreas Gohr * reverse - the list of tokens assigned to each entity 26f2bbffb5SAndreas Gohr * 27f2bbffb5SAndreas Gohr * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 28f2bbffb5SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 29f2bbffb5SAndreas Gohr * @author Tom N Harris <tnharris@whoopdedo.org> 30f2bbffb5SAndreas Gohr */ 31f2bbffb5SAndreas Gohrabstract class AbstractCollection 32f2bbffb5SAndreas Gohr{ 3395b16223SAndreas Gohr /** @var array<string|AbstractIndex> Index names or objects that have been successfully locked */ 340a9fafedSAndreas Gohr protected array $lockedIndexes = []; 350a9fafedSAndreas Gohr 36f2bbffb5SAndreas Gohr /** @var bool Has a lock been acquired for all used indexes? */ 37f2bbffb5SAndreas Gohr protected bool $isWritable = false; 38f2bbffb5SAndreas Gohr 39f2bbffb5SAndreas Gohr /** 40f2bbffb5SAndreas Gohr * Initialize the collection with the names of the indexes it manages 41f2bbffb5SAndreas Gohr * 4295b16223SAndreas Gohr * Entity and token indexes can be passed as already instantiated AbstractIndex objects 4395b16223SAndreas Gohr * for sharing between collections. When $idxToken is an object, $splitByLength must be false. 4495b16223SAndreas Gohr * 4595b16223SAndreas Gohr * @param string|AbstractIndex $idxEntity Name or instance of the primary entity index, eg. 'page' 4695b16223SAndreas Gohr * @param string|AbstractIndex $idxToken Name or instance of the secondary entity index, eg. 'w' for words 47f2bbffb5SAndreas Gohr * @param string $idxFrequency Base name of the frequency index, eg. 'i' for word frequencies 48f2bbffb5SAndreas Gohr * @param string $idxReverse Name of the reverse index, eg. 'pageword' 49f2bbffb5SAndreas Gohr * @param bool $splitByLength Whether to split token/frequency indexes by token length 5095b16223SAndreas Gohr * @throws IndexUsageException 51f2bbffb5SAndreas Gohr */ 52f2bbffb5SAndreas Gohr public function __construct( 5395b16223SAndreas Gohr protected string|AbstractIndex $idxEntity, 5495b16223SAndreas Gohr protected string|AbstractIndex $idxToken, 55d92c078cSAndreas Gohr protected string $idxFrequency = '', 56d92c078cSAndreas Gohr protected string $idxReverse = '', 57f2bbffb5SAndreas Gohr protected bool $splitByLength = false 580a9fafedSAndreas Gohr ) 590a9fafedSAndreas Gohr { 6095b16223SAndreas Gohr if ($idxToken instanceof AbstractIndex && $splitByLength) { 6195b16223SAndreas Gohr throw new IndexUsageException('Cannot split by length when using a pre-instantiated token index'); 6295b16223SAndreas Gohr } 63f2bbffb5SAndreas Gohr } 64f2bbffb5SAndreas Gohr 65f2bbffb5SAndreas Gohr /** 66f2bbffb5SAndreas Gohr * Destructor 67f2bbffb5SAndreas Gohr * 68f2bbffb5SAndreas Gohr * Ensures locks are released when the class is destroyed 69f2bbffb5SAndreas Gohr */ 70f2bbffb5SAndreas Gohr public function __destruct() 71f2bbffb5SAndreas Gohr { 72f2bbffb5SAndreas Gohr $this->unlock(); 73f2bbffb5SAndreas Gohr } 74f2bbffb5SAndreas Gohr 75f2bbffb5SAndreas Gohr /** 76f2bbffb5SAndreas Gohr * Lock all indexes for writing 77f2bbffb5SAndreas Gohr * 78f2bbffb5SAndreas Gohr * @return $this can be used for chaining 79f2bbffb5SAndreas Gohr * @throws IndexLockException 80f2bbffb5SAndreas Gohr */ 81f2bbffb5SAndreas Gohr public function lock(): static 82f2bbffb5SAndreas Gohr { 8395b16223SAndreas Gohr foreach ([ 840a9fafedSAndreas Gohr $this->idxEntity, 850a9fafedSAndreas Gohr $this->idxToken, 860a9fafedSAndreas Gohr $this->idxFrequency, 870a9fafedSAndreas Gohr $this->idxReverse 8895b16223SAndreas Gohr ] as $idx) { 8995b16223SAndreas Gohr if ($idx === '') continue; 90c66b5ec6SAndreas Gohr try { 9195b16223SAndreas Gohr if ($idx instanceof AbstractIndex) { 9295b16223SAndreas Gohr $idx->lock(); 9395b16223SAndreas Gohr $this->lockedIndexes[] = $idx; 9495b16223SAndreas Gohr } else { 9595b16223SAndreas Gohr Lock::acquire($idx); 9695b16223SAndreas Gohr $this->lockedIndexes[] = $idx; 9795b16223SAndreas Gohr } 98c66b5ec6SAndreas Gohr } catch (IndexLockException $e) { 99c66b5ec6SAndreas Gohr $this->unlock(); 100c66b5ec6SAndreas Gohr throw $e; 101c66b5ec6SAndreas Gohr } 102f2bbffb5SAndreas Gohr } 103f2bbffb5SAndreas Gohr $this->isWritable = true; 104f2bbffb5SAndreas Gohr return $this; 105f2bbffb5SAndreas Gohr } 106f2bbffb5SAndreas Gohr 107f2bbffb5SAndreas Gohr /** 1080a9fafedSAndreas Gohr * Unlock all indexes that were successfully locked 109f2bbffb5SAndreas Gohr * 110*83b3acccSAndreas Gohr * @return static 111f2bbffb5SAndreas Gohr */ 112*83b3acccSAndreas Gohr public function unlock(): static 113f2bbffb5SAndreas Gohr { 11495b16223SAndreas Gohr foreach ($this->lockedIndexes as $idx) { 11595b16223SAndreas Gohr if ($idx instanceof AbstractIndex) { 11695b16223SAndreas Gohr $idx->unlock(); 11795b16223SAndreas Gohr } else { 11895b16223SAndreas Gohr Lock::release($idx); 11995b16223SAndreas Gohr } 120f2bbffb5SAndreas Gohr } 1210a9fafedSAndreas Gohr $this->lockedIndexes = []; 122f2bbffb5SAndreas Gohr $this->isWritable = false; 123*83b3acccSAndreas Gohr return $this; 124f2bbffb5SAndreas Gohr } 125f2bbffb5SAndreas Gohr 126f2bbffb5SAndreas Gohr /** 12795b16223SAndreas Gohr * @return AbstractIndex 128c66b5ec6SAndreas Gohr * @throws IndexLockException 129f2bbffb5SAndreas Gohr */ 13095b16223SAndreas Gohr public function getEntityIndex(): AbstractIndex 131f2bbffb5SAndreas Gohr { 13295b16223SAndreas Gohr if ($this->idxEntity instanceof AbstractIndex) { 13395b16223SAndreas Gohr return $this->idxEntity; 13495b16223SAndreas Gohr } 135f2bbffb5SAndreas Gohr return new FileIndex($this->idxEntity, '', $this->isWritable); 136f2bbffb5SAndreas Gohr } 137f2bbffb5SAndreas Gohr 138f2bbffb5SAndreas Gohr /** 139f2bbffb5SAndreas Gohr * @param int|string $suffix 14095b16223SAndreas Gohr * @return AbstractIndex 141c66b5ec6SAndreas Gohr * @throws IndexLockException 142f2bbffb5SAndreas Gohr */ 14395b16223SAndreas Gohr public function getTokenIndex(int|string $suffix): AbstractIndex 144f2bbffb5SAndreas Gohr { 14595b16223SAndreas Gohr if ($this->idxToken instanceof AbstractIndex) { 14695b16223SAndreas Gohr return $this->idxToken; 14795b16223SAndreas Gohr } 148f2bbffb5SAndreas Gohr return new MemoryIndex($this->idxToken, $suffix, $this->isWritable); 149f2bbffb5SAndreas Gohr } 150f2bbffb5SAndreas Gohr 151f2bbffb5SAndreas Gohr /** 152f2bbffb5SAndreas Gohr * @param int|string $suffix 15395b16223SAndreas Gohr * @return AbstractIndex 154c66b5ec6SAndreas Gohr * @throws IndexLockException 155f2bbffb5SAndreas Gohr */ 15695b16223SAndreas Gohr public function getFrequencyIndex(int|string $suffix): AbstractIndex 157f2bbffb5SAndreas Gohr { 158f2bbffb5SAndreas Gohr return new MemoryIndex($this->idxFrequency, $suffix, $this->isWritable); 159f2bbffb5SAndreas Gohr } 160f2bbffb5SAndreas Gohr 161f2bbffb5SAndreas Gohr /** 16295b16223SAndreas Gohr * @return AbstractIndex 163c66b5ec6SAndreas Gohr * @throws IndexLockException 164f2bbffb5SAndreas Gohr */ 16595b16223SAndreas Gohr public function getReverseIndex(): AbstractIndex 166f2bbffb5SAndreas Gohr { 167f2bbffb5SAndreas Gohr return new FileIndex($this->idxReverse, '', $this->isWritable); 168f2bbffb5SAndreas Gohr } 169f2bbffb5SAndreas Gohr 170f2bbffb5SAndreas Gohr /** 171f2bbffb5SAndreas Gohr * Maximum suffix for the token indexes (eg. max word length currently stored) 172f2bbffb5SAndreas Gohr * 173f2bbffb5SAndreas Gohr * @return int 174c66b5ec6SAndreas Gohr * @throws IndexLockException 175f2bbffb5SAndreas Gohr */ 176f2bbffb5SAndreas Gohr public function getTokenIndexMaximum(): int 177f2bbffb5SAndreas Gohr { 178f2bbffb5SAndreas Gohr return $this->getTokenIndex('')->max(); // no suffix needed to access the maximum 179f2bbffb5SAndreas Gohr } 180f2bbffb5SAndreas Gohr 181f2bbffb5SAndreas Gohr /** 182f2bbffb5SAndreas Gohr * Add or update the tokens for a given entity 183f2bbffb5SAndreas Gohr * 184f2bbffb5SAndreas Gohr * The given list of tokens replaces the previously stored list for that entity. An empty list removes the 185f2bbffb5SAndreas Gohr * entity from the index. 186f2bbffb5SAndreas Gohr * 187f2bbffb5SAndreas Gohr * The update merges old and new token data. getReverseAssignments() returns all previously stored token IDs 188f2bbffb5SAndreas Gohr * with a value of 0 (see parseReverseRecord). resolveTokens() returns the new token IDs with their values. 189f2bbffb5SAndreas Gohr * After array_replace_recursive, tokens only in the old map keep value 0 — causing updateIndexes to delete 190f2bbffb5SAndreas Gohr * them from the frequency index via TupleOps::updateTuple. Tokens in the new map overwrite with their value. 191f2bbffb5SAndreas Gohr * 192f2bbffb5SAndreas Gohr * @param string $entity The name of the entity 193f2bbffb5SAndreas Gohr * @param string[] $tokens The list of tokens for this entity 194*83b3acccSAndreas Gohr * @return static 195f2bbffb5SAndreas Gohr * @throws IndexAccessException 196f2bbffb5SAndreas Gohr * @throws IndexWriteException 197f2bbffb5SAndreas Gohr * @throws IndexLockException 198f2bbffb5SAndreas Gohr */ 199*83b3acccSAndreas Gohr public function addEntity(string $entity, array $tokens): static 200f2bbffb5SAndreas Gohr { 201f2bbffb5SAndreas Gohr if (!$this->isWritable) { 202f2bbffb5SAndreas Gohr throw new IndexLockException('Indexes not locked. Forgot to call lock()?'); 203f2bbffb5SAndreas Gohr } 204f2bbffb5SAndreas Gohr 205f2bbffb5SAndreas Gohr $entityIndex = $this->getEntityIndex(); 206f2bbffb5SAndreas Gohr $entityId = $entityIndex->accessCachedValue($entity); 207f2bbffb5SAndreas Gohr 208f2bbffb5SAndreas Gohr $old = $this->getReverseAssignments($entity); 209f2bbffb5SAndreas Gohr $new = $this->resolveTokens($tokens); 210f2bbffb5SAndreas Gohr 211f2bbffb5SAndreas Gohr $merged = array_replace_recursive($old, $new); 212f2bbffb5SAndreas Gohr 213f2bbffb5SAndreas Gohr $this->updateIndexes($merged, $entityId); 214f2bbffb5SAndreas Gohr $this->saveReverseAssignments($entity, $merged); 215*83b3acccSAndreas Gohr 216*83b3acccSAndreas Gohr return $this; 217f2bbffb5SAndreas Gohr } 218f2bbffb5SAndreas Gohr 219f2bbffb5SAndreas Gohr /** 220f2bbffb5SAndreas Gohr * Resolve raw tokens into the two-level structure [group => [tokenId => frequency]] 221f2bbffb5SAndreas Gohr * 222f2bbffb5SAndreas Gohr * Calls countTokens() to get token frequencies (subclass responsibility), then groups 223f2bbffb5SAndreas Gohr * by token length if splitByLength is enabled, or under '' if not. Finally resolves 224f2bbffb5SAndreas Gohr * token strings to IDs via the appropriate token index. 225f2bbffb5SAndreas Gohr * 226f2bbffb5SAndreas Gohr * @param string[] $tokens The raw token list 227f2bbffb5SAndreas Gohr * @return array [group => [tokenId => frequency, ...], ...] 228f2bbffb5SAndreas Gohr * @throws IndexLockException 229f2bbffb5SAndreas Gohr * @throws IndexWriteException 230f2bbffb5SAndreas Gohr */ 231f2bbffb5SAndreas Gohr protected function resolveTokens(array $tokens): array 232f2bbffb5SAndreas Gohr { 233f2bbffb5SAndreas Gohr $counted = $this->countTokens($tokens); 234f2bbffb5SAndreas Gohr 235f2bbffb5SAndreas Gohr // group tokens by their index suffix 236f2bbffb5SAndreas Gohr $groups = []; 237f2bbffb5SAndreas Gohr foreach ($counted as $token => $freq) { 238f2bbffb5SAndreas Gohr $group = $this->splitByLength ? (string)Tokenizer::tokenLength($token) : ''; 239f2bbffb5SAndreas Gohr $groups[$group][$token] = $freq; 240f2bbffb5SAndreas Gohr } 241f2bbffb5SAndreas Gohr 242f2bbffb5SAndreas Gohr // resolve token strings to IDs 243f2bbffb5SAndreas Gohr $result = []; 244f2bbffb5SAndreas Gohr foreach ($groups as $group => $tokenFreqs) { 245f2bbffb5SAndreas Gohr $tokenIndex = $this->getTokenIndex($group); 246f2bbffb5SAndreas Gohr $result[$group] = []; 247f2bbffb5SAndreas Gohr foreach ($tokenFreqs as $token => $freq) { 248f2bbffb5SAndreas Gohr $tokenId = $tokenIndex->getRowID((string)$token); 249f2bbffb5SAndreas Gohr $result[$group][$tokenId] = $freq; 250f2bbffb5SAndreas Gohr } 251f2bbffb5SAndreas Gohr $tokenIndex->save(); 252f2bbffb5SAndreas Gohr } 253f2bbffb5SAndreas Gohr 254f2bbffb5SAndreas Gohr return $result; 255f2bbffb5SAndreas Gohr } 256f2bbffb5SAndreas Gohr 257f2bbffb5SAndreas Gohr /** 258f2bbffb5SAndreas Gohr * Count or deduplicate tokens and return their frequencies 259f2bbffb5SAndreas Gohr * 260f2bbffb5SAndreas Gohr * FrequencyCollections return actual occurrence counts. 261f2bbffb5SAndreas Gohr * LookupCollections deduplicate and return 1 for each token. 262f2bbffb5SAndreas Gohr * 263f2bbffb5SAndreas Gohr * @param string[] $tokens The raw token list 264f2bbffb5SAndreas Gohr * @return array [token => frequency, ...] 265f2bbffb5SAndreas Gohr */ 266f2bbffb5SAndreas Gohr abstract protected function countTokens(array $tokens): array; 267f2bbffb5SAndreas Gohr 268f2bbffb5SAndreas Gohr /** 269f2bbffb5SAndreas Gohr * Get the token assignments for a given entity from the reverse index 270f2bbffb5SAndreas Gohr * 271f2bbffb5SAndreas Gohr * Returns the parsed reverse index record. The exact structure depends on the collection type. 272f2bbffb5SAndreas Gohr * 273f2bbffb5SAndreas Gohr * @param string $entity 274f2bbffb5SAndreas Gohr * @return array 275f2bbffb5SAndreas Gohr * @throws IndexAccessException 276f2bbffb5SAndreas Gohr * @throws IndexWriteException 277c66b5ec6SAndreas Gohr * @throws IndexLockException 278f2bbffb5SAndreas Gohr */ 279f2bbffb5SAndreas Gohr public function getReverseAssignments(string $entity): array 280f2bbffb5SAndreas Gohr { 281f2bbffb5SAndreas Gohr $entityIndex = $this->getEntityIndex(); 282f2bbffb5SAndreas Gohr $entityId = $entityIndex->accessCachedValue($entity); 283f2bbffb5SAndreas Gohr 284f2bbffb5SAndreas Gohr $reverseIndex = $this->getReverseIndex(); 285f2bbffb5SAndreas Gohr $record = $reverseIndex->retrieveRow($entityId); 286f2bbffb5SAndreas Gohr 287f2bbffb5SAndreas Gohr if ($record === '') { 288f2bbffb5SAndreas Gohr return []; 289f2bbffb5SAndreas Gohr } 290f2bbffb5SAndreas Gohr 291f2bbffb5SAndreas Gohr return $this->parseReverseRecord($record); 292f2bbffb5SAndreas Gohr } 293f2bbffb5SAndreas Gohr 294f2bbffb5SAndreas Gohr /** 295f2bbffb5SAndreas Gohr * Store the reverse index info about what tokens are assigned to the entity 296f2bbffb5SAndreas Gohr * 297f2bbffb5SAndreas Gohr * @param string $entity 298f2bbffb5SAndreas Gohr * @param array $data The assignment data to store 299f2bbffb5SAndreas Gohr * @return void 300f2bbffb5SAndreas Gohr * @throws IndexAccessException 301f2bbffb5SAndreas Gohr * @throws IndexWriteException 302f2bbffb5SAndreas Gohr * @throws IndexLockException 303f2bbffb5SAndreas Gohr */ 304f2bbffb5SAndreas Gohr protected function saveReverseAssignments(string $entity, array $data): void 305f2bbffb5SAndreas Gohr { 306f2bbffb5SAndreas Gohr // remove tokens with frequency 0 (no longer assigned), then remove empty groups 307f2bbffb5SAndreas Gohr $data = array_map('array_filter', $data); 308f2bbffb5SAndreas Gohr $data = array_filter($data); 309f2bbffb5SAndreas Gohr 310f2bbffb5SAndreas Gohr $record = $this->formatReverseRecord($data); 311f2bbffb5SAndreas Gohr 312f2bbffb5SAndreas Gohr $entityIndex = $this->getEntityIndex(); 313f2bbffb5SAndreas Gohr $entityId = $entityIndex->accessCachedValue($entity); 314f2bbffb5SAndreas Gohr 315f2bbffb5SAndreas Gohr $reverseIndex = $this->getReverseIndex(); 316f2bbffb5SAndreas Gohr $reverseIndex->changeRow($entityId, $record); 317f2bbffb5SAndreas Gohr } 318f2bbffb5SAndreas Gohr 319f2bbffb5SAndreas Gohr /** 320f2bbffb5SAndreas Gohr * Parse a reverse index record into a two-level array 321f2bbffb5SAndreas Gohr * 322f2bbffb5SAndreas Gohr * The reverse index only stores which token IDs belong to an entity, not their frequencies. All values 323f2bbffb5SAndreas Gohr * in the returned array are set to 0. This is intentional: when merged with new data in addEntity(), 324f2bbffb5SAndreas Gohr * tokens absent from the new data retain 0, signaling deletion from the frequency index. 325f2bbffb5SAndreas Gohr * 326f2bbffb5SAndreas Gohr * For split collections the format is "group*tokenId:group*tokenId:..." where group is the token length. 327f2bbffb5SAndreas Gohr * For non-split collections the group prefix is omitted: "tokenId:tokenId:..." 328f2bbffb5SAndreas Gohr * This mirrors how TupleOps omits *1 for frequency 1. 329f2bbffb5SAndreas Gohr * 330f2bbffb5SAndreas Gohr * @param string $record The raw reverse index record 331f2bbffb5SAndreas Gohr * @return array [group => [tokenId => 0, ...], ...] 332f2bbffb5SAndreas Gohr */ 333f2bbffb5SAndreas Gohr protected function parseReverseRecord(string $record): array 334f2bbffb5SAndreas Gohr { 335f2bbffb5SAndreas Gohr $result = []; 336f2bbffb5SAndreas Gohr foreach (explode(':', $record) as $entry) { 337f2bbffb5SAndreas Gohr $parts = explode('*', $entry, 2); 338f2bbffb5SAndreas Gohr $tokenId = array_pop($parts); 339f2bbffb5SAndreas Gohr $group = array_pop($parts) ?? ''; 340f2bbffb5SAndreas Gohr $result[$group][$tokenId] = 0; 341f2bbffb5SAndreas Gohr } 342f2bbffb5SAndreas Gohr return $result; 343f2bbffb5SAndreas Gohr } 344f2bbffb5SAndreas Gohr 345f2bbffb5SAndreas Gohr /** 346f2bbffb5SAndreas Gohr * Format a two-level array into a reverse index record string 347f2bbffb5SAndreas Gohr * 348f2bbffb5SAndreas Gohr * @param array $data [group => [tokenId => freq, ...], ...] 349f2bbffb5SAndreas Gohr * @return string The formatted record 350f2bbffb5SAndreas Gohr */ 351f2bbffb5SAndreas Gohr protected function formatReverseRecord(array $data): string 352f2bbffb5SAndreas Gohr { 353f2bbffb5SAndreas Gohr $parts = []; 354f2bbffb5SAndreas Gohr foreach ($data as $group => $tokens) { 355f2bbffb5SAndreas Gohr $prefix = $group === '' ? '' : "$group*"; 356f2bbffb5SAndreas Gohr foreach (array_keys($tokens) as $tokenId) { 357f2bbffb5SAndreas Gohr $parts[] = $prefix . $tokenId; 358f2bbffb5SAndreas Gohr } 359f2bbffb5SAndreas Gohr } 360f2bbffb5SAndreas Gohr return implode(':', $parts); 361f2bbffb5SAndreas Gohr } 362f2bbffb5SAndreas Gohr 363f2bbffb5SAndreas Gohr /** 364f2bbffb5SAndreas Gohr * Update frequency indexes with the given data 365f2bbffb5SAndreas Gohr * 366f2bbffb5SAndreas Gohr * Iterates over the two-level structure [group => [tokenId => freq]] and updates the 367f2bbffb5SAndreas Gohr * corresponding frequency index for each group. A frequency of 0 removes the entity 368f2bbffb5SAndreas Gohr * from that token's frequency record. 369f2bbffb5SAndreas Gohr * 370f2bbffb5SAndreas Gohr * @param array $data [group => [tokenId => frequency, ...], ...] 371f2bbffb5SAndreas Gohr * @param int $entityId The entity ID 372f2bbffb5SAndreas Gohr * @throws IndexLockException 373f2bbffb5SAndreas Gohr * @throws IndexWriteException 374f2bbffb5SAndreas Gohr */ 375f2bbffb5SAndreas Gohr protected function updateIndexes(array $data, int $entityId): void 376f2bbffb5SAndreas Gohr { 377f2bbffb5SAndreas Gohr foreach ($data as $group => $tokens) { 378f2bbffb5SAndreas Gohr $freqIndex = $this->getFrequencyIndex($group); 379f2bbffb5SAndreas Gohr foreach ($tokens as $tokenId => $freq) { 380f2bbffb5SAndreas Gohr $record = $freqIndex->retrieveRow($tokenId); 381f2bbffb5SAndreas Gohr $record = TupleOps::updateTuple($record, $entityId, $freq); 382f2bbffb5SAndreas Gohr $freqIndex->changeRow($tokenId, $record); 383f2bbffb5SAndreas Gohr } 384f2bbffb5SAndreas Gohr $freqIndex->save(); 385f2bbffb5SAndreas Gohr } 386f2bbffb5SAndreas Gohr } 387f2bbffb5SAndreas Gohr} 388