1<?php 2 3namespace dokuwiki\Search\Collection; 4 5use dokuwiki\Search\Exception\IndexAccessException; 6use dokuwiki\Search\Exception\IndexIntegrityException; 7use dokuwiki\Search\Exception\IndexLockException; 8use dokuwiki\Search\Exception\IndexWriteException; 9 10/** 11 * Abstract collection for direct 1:1 entity-token mappings 12 * 13 * In a direct collection each entity has exactly one token stored at the entity's position 14 * in the token index (entity.RID === token.RID). No frequency or reverse indexes are used. 15 * 16 * Example: each page has exactly one title. 17 * 18 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 19 * @author Andreas Gohr <andi@splitbrain.org> 20 */ 21abstract class DirectCollection extends AbstractCollection 22{ 23 /** @inheritdoc */ 24 public function checkIntegrity(): void 25 { 26 $entityIndex = $this->getEntityIndex(); 27 $tokenIndex = $this->getTokenIndex(); 28 29 if ($entityIndex->exists() && $tokenIndex->exists()) { 30 $ec = count($entityIndex); 31 $tc = count($tokenIndex); 32 if ($ec !== $tc) { 33 throw new IndexIntegrityException( 34 "Entity count ($ec) != token count ($tc)" 35 ); 36 } 37 } 38 } 39 40 /** 41 * Store a single token for the given entity 42 * 43 * Takes the first token from the list and writes it directly at the entity's position 44 * in the token index. An empty list stores an empty string. 45 * 46 * @param string $entity The name of the entity 47 * @param string[] $tokens The list of tokens (only the first is used) 48 * @return static 49 * @throws IndexLockException 50 * @throws IndexAccessException 51 * @throws IndexWriteException 52 */ 53 public function addEntity(string $entity, array $tokens): static 54 { 55 if (!$this->isWritable) { 56 throw new IndexLockException('Indexes not locked. Forgot to call lock()?'); 57 } 58 59 $entityIndex = $this->getEntityIndex(); 60 $entityId = $entityIndex->accessCachedValue($entity); 61 62 $token = $tokens[0] ?? ''; 63 $tokenIndex = $this->getTokenIndex(); 64 $tokenIndex->changeRow($entityId, $token); 65 $tokenIndex->save(); 66 67 return $this; 68 } 69 70 /** 71 * Get the token stored for the given entity 72 * 73 * @param string $entity The name of the entity 74 * @return string The stored token, or empty string if none 75 * @throws IndexAccessException 76 * @throws IndexLockException 77 * @throws IndexWriteException 78 */ 79 public function getToken(string $entity): string 80 { 81 $entityIndex = $this->getEntityIndex(); 82 $entityId = $entityIndex->accessCachedValue($entity); 83 84 $tokenIndex = $this->getTokenIndex(); 85 return $tokenIndex->retrieveRow($entityId); 86 } 87 88 /** @inheritdoc */ 89 public function resolveTokenFrequencies(int $group, array $tokenIds): array 90 { 91 // In a DirectCollection, token RID = entity RID, frequency is always 1 92 $result = []; 93 foreach ($tokenIds as $tokenId) { 94 $result[$tokenId] = [$tokenId => 1]; 95 } 96 return $result; 97 } 98 99 /** @inheritdoc */ 100 public function getEntitiesWithData(): array 101 { 102 $entityIndex = $this->getEntityIndex(); 103 $tokenIndex = $this->getTokenIndex(); 104 105 $entities = []; 106 foreach ($tokenIndex as $entityId => $token) { 107 if ($token === '') continue; 108 $name = $entityIndex->retrieveRow($entityId); 109 if ($name !== '') { 110 $entities[] = $name; 111 } 112 } 113 return $entities; 114 } 115 116 /** 117 * Frequency histogram of the tokens in this collection 118 * 119 * Each entity holds exactly one token, so this counts how many entities 120 * share each token value. 121 * 122 * @param int $min minimum frequency a token must reach to be included 123 * @param int $max maximum frequency to include, 0 for no upper limit 124 * @param int $minlen minimum token length to include 125 * @return array<string, int> token => frequency, ordered by frequency descending 126 * @throws IndexLockException 127 */ 128 public function histogram(int $min = 1, int $max = 0, int $minlen = 3): array 129 { 130 if ($min < 1) $min = 1; 131 if ($max < $min) $max = 0; 132 if ($minlen < 1) $minlen = 1; 133 134 $tokenIndex = $this->getTokenIndex(); 135 if (!$tokenIndex->exists()) return []; 136 137 // an empty token is shorter than $minlen (>= 1) and so is filtered here 138 $counts = []; 139 foreach ($tokenIndex as $token) { 140 if (strlen($token) < $minlen) continue; 141 $counts[$token] = ($counts[$token] ?? 0) + 1; 142 } 143 144 $result = array_filter( 145 $counts, 146 static fn($freq) => $freq >= $min && (!$max || $freq <= $max) 147 ); 148 149 arsort($result); 150 return $result; 151 } 152 153 /** 154 * Not actually used, because we override addEntity() to directly write the token. 155 * @inheritdoc 156 */ 157 protected function countTokens(array $tokens): array 158 { 159 $token = $tokens[0] ?? ''; 160 return [$token => 1]; 161 } 162} 163