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