xref: /dokuwiki/_test/tests/Search/Collection/CollectionSearchTest.php (revision 42685d0d01673fb531341567389d20918ec9bfd3)
1<?php
2
3namespace dokuwiki\test\Search\Collection;
4
5use dokuwiki\Search\Collection\CollectionSearch;
6use dokuwiki\Search\Index\MemoryIndex;
7use dokuwiki\Search\Tokenizer;
8
9class CollectionSearchTest extends \DokuWikiTest
10{
11
12    public function testExactTerm()
13    {
14        // add some content to the indexes
15        $collection = new MockFrequencyCollection('page', 'w', 'i', 'pageword');
16        $collection->lock();
17        $collection->addEntity('page1', ['dokuwiki', 'dokuwiki', 'dokuwikis', 'doku', 'wiki']);
18        $collection->addEntity('page2', ['dokuwiki', 'other', 'words']);
19        $collection->unlock();
20
21        // add search term
22        $search = new CollectionSearch($collection);
23        $term = $search->addTerm('dokuwiki');
24
25        // execute search
26        $search->execute();
27
28        // exact search should only match one token
29        $this->assertEquals(['dokuwiki'], $term->getTokens());
30        // the dokuwiki token is two times on page1 and 1 time on page2
31        $this->assertEquals(['page1' => 2, 'page2' => 1], $term->getEntityFrequencies());
32        // full detail available
33        $this->assertEquals(['dokuwiki' => 2], $term->getMatches()['page1']);
34        $this->assertEquals(['dokuwiki' => 1], $term->getMatches()['page2']);
35
36    }
37
38    public function testWildcardSearch()
39    {
40        // page1 has: dokuwiki(x2), dokuwikis, doku, wiki
41        // page2 has: dokuwiki, other, words
42        $collection = new MockFrequencyCollection('wc_page', 'wc_w', 'wc_i', 'wc_pageword');
43        $collection->lock();
44        $collection->addEntity('page1', ['dokuwiki', 'dokuwiki', 'dokuwikis', 'doku', 'wiki']);
45        $collection->addEntity('page2', ['dokuwiki', 'other', 'words']);
46        $collection->unlock();
47
48        $search = new CollectionSearch($collection);
49        $endWild = $search->addTerm('doku*');
50        $startWild = $search->addTerm('*wiki');
51        $bothWild = $search->addTerm('*kuwi*');
52        $search->execute();
53
54        // doku* should match: doku(4), dokuwiki(8), dokuwikis(9)
55        $endTokens = $endWild->getTokens();
56        sort($endTokens);
57        $this->assertEquals(['doku', 'dokuwiki', 'dokuwikis'], $endTokens);
58        // page1 has doku(1) + dokuwiki(2) + dokuwikis(1) = 4, page2 has dokuwiki(1) = 1
59        $this->assertEquals(['page1' => 4, 'page2' => 1], $endWild->getEntityFrequencies());
60
61        // *wiki should match: dokuwiki(8), wiki(4)
62        $startTokens = $startWild->getTokens();
63        sort($startTokens);
64        $this->assertEquals(['dokuwiki', 'wiki'], $startTokens);
65        // page1 has dokuwiki(2) + wiki(1) = 3, page2 has dokuwiki(1) = 1
66        $this->assertEquals(['page1' => 3, 'page2' => 1], $startWild->getEntityFrequencies());
67
68        // *kuwi* should match: dokuwiki(8), dokuwikis(9)
69        $bothTokens = $bothWild->getTokens();
70        sort($bothTokens);
71        $this->assertEquals(['dokuwiki', 'dokuwikis'], $bothTokens);
72        // page1 has dokuwiki(2) + dokuwikis(1) = 3, page2 has dokuwiki(1) = 1
73        $this->assertEquals(['page1' => 3, 'page2' => 1], $bothWild->getEntityFrequencies());
74    }
75
76    public function testWildcardSearchAcrossMultiDigitLengthGroups()
77    {
78        $collection = new MockFrequencyCollection('wc10_page', 'wc10_w', 'wc10_i', 'wc10_pageword');
79        $collection->lock();
80        $collection->addEntity('page1', ['encyclopedia', 'encyclopedic', 'short']);
81        $collection->addEntity('page2', ['encyclopedia', 'different']);
82        $collection->unlock();
83
84        $search = new CollectionSearch($collection);
85        $term = $search->addTerm('encyclo*');
86        $search->execute();
87
88        $tokens = $term->getTokens();
89        sort($tokens);
90        $this->assertEquals(['encyclopedia', 'encyclopedic'], $tokens);
91        $this->assertEquals(['page1' => 2, 'page2' => 1], $term->getEntityFrequencies());
92    }
93
94    /**
95     * Index a real text file via the Tokenizer and search it
96     */
97    public function testTokenizedPageSearch()
98    {
99        $text = file_get_contents(__DIR__ . '/../data/searchtest.txt');
100        $tokens = Tokenizer::getWords($text);
101
102        $collection = new MockFrequencyCollection('tp_page', 'tp_w', 'tp_i', 'tp_pageword');
103        $collection->lock();
104        $collection->addEntity('search:test', $tokens);
105        $collection->unlock();
106
107        $search = new CollectionSearch($collection);
108        $exact = $search->addTerm('dokuwiki');
109        $wild = $search->addTerm('plugin*');
110        $search->execute();
111
112        // "dokuwiki" appears 4 times in the text (case-insensitive tokenization)
113        $this->assertEquals(['dokuwiki'], $exact->getTokens());
114        $this->assertEquals(['search:test' => 4], $exact->getEntityFrequencies());
115
116        // "plugin*" should match "plugins" (7 chars) and "plugin" would be too if present
117        $wildTokens = $wild->getTokens();
118        $this->assertContains('plugins', $wildTokens);
119        $this->assertNotEmpty($wild->getEntityFrequencies());
120        $this->assertArrayHasKey('search:test', $wild->getEntityFrequencies());
121    }
122
123    public function testNoMatchReturnsEmptyFrequencies()
124    {
125        $collection = new MockFrequencyCollection('nm_page', 'nm_w', 'nm_i', 'nm_pageword');
126        $collection->lock();
127        $collection->addEntity('page1', ['alpha', 'beta', 'gamma']);
128        $collection->unlock();
129
130        $search = new CollectionSearch($collection);
131        $term = $search->addTerm('zzzznotfound');
132        $search->execute();
133
134        $this->assertEmpty($term->getTokens());
135        $this->assertEmpty($term->getEntityFrequencies());
136        $this->assertEmpty($term->getMatches());
137    }
138
139    // --- metadata-style search tests (using addTerm/execute without length restrictions) ---
140
141    /**
142     * Exact search on a non-split LookupCollection
143     */
144    public function testMetadataExact()
145    {
146        $collection = new MockLookupCollection('le_entity', 'le_token', 'le_freq', 'le_reverse');
147        $collection->lock();
148        $collection->addEntity('wiki:start', ['wiki:syntax', 'wiki:welcome']);
149        $collection->addEntity('wiki:other', ['wiki:syntax']);
150        $collection->unlock();
151
152        $search = new CollectionSearch($collection);
153        $term = $search->addTerm('wiki:syntax');
154        $search->execute();
155
156        $pages = array_keys($term->getEntityFrequencies());
157        sort($pages);
158        $this->assertEquals(['wiki:other', 'wiki:start'], $pages);
159    }
160
161    /**
162     * Wildcard search on a non-split LookupCollection
163     */
164    public function testMetadataWildcard()
165    {
166        $collection = new MockLookupCollection('lw_entity', 'lw_token', 'lw_freq', 'lw_reverse');
167        $collection->lock();
168        $collection->addEntity('wiki:start', ['wiki:syntax', 'wiki:welcome']);
169        $collection->addEntity('wiki:other', ['wiki:syntax', 'other:page']);
170        $collection->unlock();
171
172        // end wildcard: wiki:* matches wiki:syntax and wiki:welcome
173        $search = new CollectionSearch($collection);
174        $term = $search->addTerm('wiki:*');
175        $search->execute();
176
177        $pages = array_keys($term->getEntityFrequencies());
178        sort($pages);
179        // wiki:start has both tokens (freq 2), wiki:other has wiki:syntax (freq 1)
180        $this->assertEquals(['wiki:other', 'wiki:start'], $pages);
181
182        // start wildcard: *syntax matches only wiki:syntax
183        $search2 = new CollectionSearch($collection);
184        $term2 = $search2->addTerm('*syntax');
185        $search2->execute();
186
187        $pages2 = array_keys($term2->getEntityFrequencies());
188        sort($pages2);
189        $this->assertEquals(['wiki:other', 'wiki:start'], $pages2);
190    }
191
192    /**
193     * Case-insensitive search on a non-split LookupCollection
194     */
195    public function testMetadataCaseInsensitive()
196    {
197        $collection = new MockLookupCollection('lc_entity', 'lc_token', 'lc_freq', 'lc_reverse');
198        $collection->lock();
199        $collection->addEntity('wiki:start', ['Apple', 'Banana']);
200        $collection->addEntity('wiki:other', ['Cherry', 'Apple Pie']);
201        $collection->unlock();
202
203        $search = new CollectionSearch($collection);
204        $search->caseInsensitive();
205        $term = $search->addTerm('*apple*');
206        $search->execute();
207
208        $pages = array_keys($term->getEntityFrequencies());
209        sort($pages);
210        $this->assertEquals(['wiki:other', 'wiki:start'], $pages);
211    }
212
213    /**
214     * Search on a DirectCollection (title-style 1:1 mapping)
215     */
216    public function testSearchOnDirectCollection()
217    {
218        $collection = new MockDirectCollection('ld_entity', 'ld_token');
219        $collection->lock();
220        $collection->addEntity('wiki:start', ['Welcome to DokuWiki']);
221        $collection->addEntity('wiki:syntax', ['Formatting Syntax']);
222        $collection->addEntity('wiki:other', ['Other Page']);
223        $collection->unlock();
224
225        // exact match
226        $search = new CollectionSearch($collection);
227        $term = $search->addTerm('Welcome to DokuWiki');
228        $search->execute();
229        $this->assertEquals(['wiki:start'], array_keys($term->getEntityFrequencies()));
230
231        // wildcard match
232        $search2 = new CollectionSearch($collection);
233        $term2 = $search2->addTerm('*Syntax');
234        $search2->execute();
235        $this->assertEquals(['wiki:syntax'], array_keys($term2->getEntityFrequencies()));
236
237        // case-insensitive substring match
238        $search3 = new CollectionSearch($collection);
239        $search3->caseInsensitive();
240        $term3 = $search3->addTerm('*wiki*');
241        $search3->execute();
242        $this->assertEquals(['wiki:start'], array_keys($term3->getEntityFrequencies()));
243    }
244
245    /**
246     * Multiple terms in a single search
247     */
248    public function testMultipleTerms()
249    {
250        $collection = new MockLookupCollection('lm_entity', 'lm_token', 'lm_freq', 'lm_reverse');
251        $collection->lock();
252        $collection->addEntity('wiki:start', ['wiki:syntax', 'wiki:welcome']);
253        $collection->addEntity('wiki:other', ['wiki:syntax']);
254        $collection->unlock();
255
256        $search = new CollectionSearch($collection);
257        $term1 = $search->addTerm('wiki:syntax');
258        $term2 = $search->addTerm('wiki:welcome');
259        $term3 = $search->addTerm('nonexistent');
260        $search->execute();
261
262        $syntax = array_keys($term1->getEntityFrequencies());
263        sort($syntax);
264        $this->assertEquals(['wiki:other', 'wiki:start'], $syntax);
265        $this->assertEquals(['wiki:start'], array_keys($term2->getEntityFrequencies()));
266        $this->assertEquals([], array_keys($term3->getEntityFrequencies()));
267    }
268
269    /**
270     * Search on a split FrequencyCollection
271     */
272    public function testSearchOnSplitCollection()
273    {
274        $collection = new MockFrequencyCollection('ls_page', 'ls_w', 'ls_i', 'ls_pageword');
275        $collection->lock();
276        $collection->addEntity('page1', ['dokuwiki', 'wiki', 'doku']);
277        $collection->addEntity('page2', ['dokuwiki', 'other']);
278        $collection->unlock();
279
280        $search = new CollectionSearch($collection);
281        $term = $search->addTerm('dokuwiki');
282        $search->execute();
283
284        $pages = array_keys($term->getEntityFrequencies());
285        sort($pages);
286        $this->assertEquals(['page1', 'page2'], $pages);
287    }
288
289    /**
290     * Searching an empty collection returns no results
291     */
292    public function testSearchEmptyCollection()
293    {
294        $collection = new MockFrequencyCollection('empty_page', 'empty_w', 'empty_i', 'empty_pw');
295
296        $search = new CollectionSearch($collection);
297        $term = $search->addTerm('anything');
298        $search->execute();
299        $this->assertEquals([], $term->getEntityFrequencies());
300    }
301
302    /**
303     * Search on an empty collection returns empty frequencies
304     */
305    public function testSearchEmptyCollection2()
306    {
307        $collection = new MockFrequencyCollection('empty2_page', 'empty2_w', 'empty2_i', 'empty2_pw');
308
309        $search = new CollectionSearch($collection);
310        $term = $search->addTerm('anything');
311        $search->execute();
312        $this->assertEquals([], $term->getEntityFrequencies());
313    }
314}
315