xref: /dokuwiki/inc/Search/Collection/DirectCollection.php (revision 61dea710fc1da6e08c9033ff498556a8a01d3b77)
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