xref: /dokuwiki/inc/Search/Collection/DirectCollection.php (revision fe58309edafb067d90f3f40ef3d416100d558a04)
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        $entityIds = [];
106        foreach ($tokenIndex as $entityId => $token) {
107            if ($token !== '') $entityIds[] = $entityId;
108        }
109
110        $names = $entityIndex->retrieveRows($entityIds);
111        return array_values(array_filter($names, static fn($v) => $v !== ''));
112    }
113
114    /**
115     * Frequency histogram of the tokens in this collection
116     *
117     * Each entity holds exactly one token, so this counts how many entities
118     * share each token value.
119     *
120     * @param int $min minimum frequency a token must reach to be included
121     * @param int $max maximum frequency to include, 0 for no upper limit
122     * @param int $minlen minimum token length to include
123     * @return array<string, int> token => frequency, ordered by frequency descending
124     * @throws IndexLockException
125     */
126    public function histogram(int $min = 1, int $max = 0, int $minlen = 3): array
127    {
128        if ($min < 1) $min = 1;
129        if ($max < $min) $max = 0;
130        if ($minlen < 1) $minlen = 1;
131
132        $tokenIndex = $this->getTokenIndex();
133        if (!$tokenIndex->exists()) return [];
134
135        // an empty token is shorter than $minlen (>= 1) and so is filtered here
136        $counts = [];
137        foreach ($tokenIndex as $token) {
138            if (strlen($token) < $minlen) continue;
139            $counts[$token] = ($counts[$token] ?? 0) + 1;
140        }
141
142        $result = array_filter(
143            $counts,
144            static fn($freq) => $freq >= $min && (!$max || $freq <= $max)
145        );
146
147        arsort($result);
148        return $result;
149    }
150
151    /**
152     * Not actually used, because we override addEntity() to directly write the token.
153     * @inheritdoc
154     */
155    protected function countTokens(array $tokens): array
156    {
157        $token = $tokens[0] ?? '';
158        return [$token => 1];
159    }
160}
161