1ede46466SAndreas Gohr<?php 2ede46466SAndreas Gohr 3ede46466SAndreas Gohrnamespace dokuwiki\test\Search\Query; 4ede46466SAndreas Gohr 5ede46466SAndreas Gohruse dokuwiki\Search\Collection\Term; 6ede46466SAndreas Gohruse dokuwiki\Search\Query\QueryEvaluator; 7ede46466SAndreas Gohr 8ede46466SAndreas Gohr/** 9ede46466SAndreas Gohr * Tests for the QueryEvaluator class 10ede46466SAndreas Gohr * 11ede46466SAndreas Gohr * These tests verify RPN evaluation with typed stack entries (page sets, 12ede46466SAndreas Gohr * namespace predicates, negated wrappers) independent of actual index data. 13ede46466SAndreas Gohr */ 14ede46466SAndreas Gohrclass QueryEvaluatorTest extends \DokuWikiTest 15ede46466SAndreas Gohr{ 16ede46466SAndreas Gohr /** 17ede46466SAndreas Gohr * Create a Term with pre-resolved entity frequencies 18ede46466SAndreas Gohr * 19ede46466SAndreas Gohr * @param string $word the word this term represents 20ede46466SAndreas Gohr * @param array $frequencies [pageName => frequency] 21ede46466SAndreas Gohr * @return Term 22ede46466SAndreas Gohr */ 23ede46466SAndreas Gohr protected function makeTerm(string $word, array $frequencies): Term 24ede46466SAndreas Gohr { 25ede46466SAndreas Gohr $term = new Term($word); 26ede46466SAndreas Gohr foreach ($frequencies as $page => $freq) { 27*1148921dSAndreas Gohr $term->addMatch($page, $word, $freq); 28ede46466SAndreas Gohr } 29ede46466SAndreas Gohr return $term; 30ede46466SAndreas Gohr } 31ede46466SAndreas Gohr 32ede46466SAndreas Gohr // region Basic word lookups 33ede46466SAndreas Gohr 34ede46466SAndreas Gohr public function testSingleWord() 35ede46466SAndreas Gohr { 36ede46466SAndreas Gohr $terms = [ 37ede46466SAndreas Gohr 'dokuwiki' => $this->makeTerm('dokuwiki', ['page1' => 3, 'page2' => 1]), 38ede46466SAndreas Gohr ]; 39ede46466SAndreas Gohr $rpn = ['W+:dokuwiki']; 40ede46466SAndreas Gohr 41ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 42ede46466SAndreas Gohr $result = $evaluator->evaluate(); 43ede46466SAndreas Gohr 44ede46466SAndreas Gohr $this->assertEquals(['page1' => 3, 'page2' => 1], $result); 45ede46466SAndreas Gohr } 46ede46466SAndreas Gohr 47ede46466SAndreas Gohr public function testUnknownWord() 48ede46466SAndreas Gohr { 49ede46466SAndreas Gohr $terms = []; 50ede46466SAndreas Gohr $rpn = ['W+:nonexistent']; 51ede46466SAndreas Gohr 52ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 53ede46466SAndreas Gohr $result = $evaluator->evaluate(); 54ede46466SAndreas Gohr 55ede46466SAndreas Gohr $this->assertEquals([], $result); 56ede46466SAndreas Gohr } 57ede46466SAndreas Gohr 58ede46466SAndreas Gohr // endregion 59ede46466SAndreas Gohr 60ede46466SAndreas Gohr // region AND operation 61ede46466SAndreas Gohr 62ede46466SAndreas Gohr public function testAndTwoWords() 63ede46466SAndreas Gohr { 64ede46466SAndreas Gohr $terms = [ 65ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 2, 'page2' => 3, 'page3' => 1]), 66ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page1' => 1, 'page3' => 4]), 67ede46466SAndreas Gohr ]; 68ede46466SAndreas Gohr // foo AND bar → pages in both, scores summed 69ede46466SAndreas Gohr $rpn = ['W+:foo', 'W+:bar', 'AND']; 70ede46466SAndreas Gohr 71ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 72ede46466SAndreas Gohr $result = $evaluator->evaluate(); 73ede46466SAndreas Gohr 74ede46466SAndreas Gohr $this->assertEquals(['page1' => 3, 'page3' => 5], $result); 75ede46466SAndreas Gohr } 76ede46466SAndreas Gohr 77ede46466SAndreas Gohr // endregion 78ede46466SAndreas Gohr 79ede46466SAndreas Gohr // region OR operation 80ede46466SAndreas Gohr 81ede46466SAndreas Gohr public function testOrTwoWords() 82ede46466SAndreas Gohr { 83ede46466SAndreas Gohr $terms = [ 84ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 2, 'page2' => 3]), 85ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page1' => 1, 'page3' => 4]), 86ede46466SAndreas Gohr ]; 87ede46466SAndreas Gohr // foo OR bar → union, scores summed where overlapping 88ede46466SAndreas Gohr $rpn = ['W+:foo', 'W+:bar', 'OR']; 89ede46466SAndreas Gohr 90ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 91ede46466SAndreas Gohr $result = $evaluator->evaluate(); 92ede46466SAndreas Gohr 93ede46466SAndreas Gohr $this->assertEquals(['page1' => 3, 'page2' => 3, 'page3' => 4], $result); 94ede46466SAndreas Gohr } 95ede46466SAndreas Gohr 96ede46466SAndreas Gohr // endregion 97ede46466SAndreas Gohr 98ede46466SAndreas Gohr // region NOT with AND (subtraction) 99ede46466SAndreas Gohr 100ede46466SAndreas Gohr public function testNotWithAnd() 101ede46466SAndreas Gohr { 102ede46466SAndreas Gohr // "foo -bar" → foo AND NOT bar → foo minus bar 103ede46466SAndreas Gohr $terms = [ 104ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 2, 'page2' => 3, 'page3' => 1]), 105ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page2' => 1]), 106ede46466SAndreas Gohr ]; 107ede46466SAndreas Gohr // RPN: foo bar NOT AND 108ede46466SAndreas Gohr $rpn = ['W+:foo', 'W-:bar', 'NOT', 'AND']; 109ede46466SAndreas Gohr 110ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 111ede46466SAndreas Gohr $result = $evaluator->evaluate(); 112ede46466SAndreas Gohr 113ede46466SAndreas Gohr $this->assertEquals(['page1' => 2, 'page3' => 1], $result); 114ede46466SAndreas Gohr } 115ede46466SAndreas Gohr 116ede46466SAndreas Gohr public function testNegatedGroupWithAnd() 117ede46466SAndreas Gohr { 118ede46466SAndreas Gohr // "baz -(foo OR bar)" → baz AND NOT(foo OR bar) → baz minus (foo ∪ bar) 119ede46466SAndreas Gohr $terms = [ 120ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 1, 'page2' => 2]), 121ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page2' => 1, 'page3' => 3]), 122ede46466SAndreas Gohr 'baz' => $this->makeTerm('baz', ['page1' => 5, 'page2' => 4, 'page3' => 2, 'page4' => 1]), 123ede46466SAndreas Gohr ]; 124ede46466SAndreas Gohr // RPN: foo bar OR NOT baz AND 125ede46466SAndreas Gohr $rpn = ['W+:foo', 'W+:bar', 'OR', 'NOT', 'W+:baz', 'AND']; 126ede46466SAndreas Gohr 127ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 128ede46466SAndreas Gohr $result = $evaluator->evaluate(); 129ede46466SAndreas Gohr 130ede46466SAndreas Gohr // page1, page2, page3 are in (foo ∪ bar), so only page4 remains 131ede46466SAndreas Gohr $this->assertEquals(['page4' => 1], $result); 132ede46466SAndreas Gohr } 133ede46466SAndreas Gohr 134ede46466SAndreas Gohr // endregion 135ede46466SAndreas Gohr 136ede46466SAndreas Gohr // region Namespace filtering 137ede46466SAndreas Gohr 138ede46466SAndreas Gohr public function testNamespaceInclude() 139ede46466SAndreas Gohr { 140ede46466SAndreas Gohr // "foo @wiki:" → foo AND namespace wiki: 141ede46466SAndreas Gohr $terms = [ 142ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['wiki:page1' => 2, 'other:page2' => 3, 'wiki:sub:page3' => 1]), 143ede46466SAndreas Gohr ]; 144ede46466SAndreas Gohr // RPN: foo N+:wiki AND 145ede46466SAndreas Gohr $rpn = ['W+:foo', 'N+:wiki', 'AND']; 146ede46466SAndreas Gohr 147ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 148ede46466SAndreas Gohr $result = $evaluator->evaluate(); 149ede46466SAndreas Gohr 150ede46466SAndreas Gohr $this->assertEquals(['wiki:page1' => 2, 'wiki:sub:page3' => 1], $result); 151ede46466SAndreas Gohr } 152ede46466SAndreas Gohr 153ede46466SAndreas Gohr public function testNamespaceExclude() 154ede46466SAndreas Gohr { 155ede46466SAndreas Gohr // "foo ^wiki:" → foo AND NOT namespace wiki: 156ede46466SAndreas Gohr $terms = [ 157ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['wiki:page1' => 2, 'other:page2' => 3, 'wiki:sub:page3' => 1]), 158ede46466SAndreas Gohr ]; 159ede46466SAndreas Gohr // RPN: foo N+:wiki NOT AND 160ede46466SAndreas Gohr $rpn = ['W+:foo', 'N+:wiki', 'NOT', 'AND']; 161ede46466SAndreas Gohr 162ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 163ede46466SAndreas Gohr $result = $evaluator->evaluate(); 164ede46466SAndreas Gohr 165ede46466SAndreas Gohr $this->assertEquals(['other:page2' => 3], $result); 166ede46466SAndreas Gohr } 167ede46466SAndreas Gohr 168ede46466SAndreas Gohr // endregion 169ede46466SAndreas Gohr 170ede46466SAndreas Gohr // region Combined queries 171ede46466SAndreas Gohr 172ede46466SAndreas Gohr public function testOrThenNot() 173ede46466SAndreas Gohr { 174ede46466SAndreas Gohr // "(foo OR bar) -baz" → (foo OR bar) AND NOT baz 175ede46466SAndreas Gohr $terms = [ 176ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 2, 'page2' => 1]), 177ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page2' => 3, 'page3' => 4]), 178ede46466SAndreas Gohr 'baz' => $this->makeTerm('baz', ['page2' => 1]), 179ede46466SAndreas Gohr ]; 180ede46466SAndreas Gohr // RPN: foo bar OR baz NOT AND 181ede46466SAndreas Gohr $rpn = ['W+:foo', 'W+:bar', 'OR', 'W-:baz', 'NOT', 'AND']; 182ede46466SAndreas Gohr 183ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 184ede46466SAndreas Gohr $result = $evaluator->evaluate(); 185ede46466SAndreas Gohr 186ede46466SAndreas Gohr $this->assertEquals(['page1' => 2, 'page3' => 4], $result); 187ede46466SAndreas Gohr } 188ede46466SAndreas Gohr 189ede46466SAndreas Gohr public function testWordWithNamespaceAndNot() 190ede46466SAndreas Gohr { 191ede46466SAndreas Gohr // "foo -bar @wiki:" → foo AND NOT bar AND @wiki: 192ede46466SAndreas Gohr $terms = [ 193ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', [ 194ede46466SAndreas Gohr 'wiki:a' => 5, 'wiki:b' => 3, 'other:c' => 2, 'wiki:d' => 1, 195ede46466SAndreas Gohr ]), 196ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['wiki:b' => 1]), 197ede46466SAndreas Gohr ]; 198ede46466SAndreas Gohr // RPN: foo bar NOT AND N+:wiki AND 199ede46466SAndreas Gohr $rpn = ['W+:foo', 'W-:bar', 'NOT', 'AND', 'N+:wiki', 'AND']; 200ede46466SAndreas Gohr 201ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 202ede46466SAndreas Gohr $result = $evaluator->evaluate(); 203ede46466SAndreas Gohr 204ede46466SAndreas Gohr // foo minus bar = wiki:a, other:c, wiki:d 205ede46466SAndreas Gohr // filtered to wiki: = wiki:a, wiki:d 206ede46466SAndreas Gohr $this->assertEquals(['wiki:a' => 5, 'wiki:d' => 1], $result); 207ede46466SAndreas Gohr } 208ede46466SAndreas Gohr 209ede46466SAndreas Gohr public function testNamespaceDoesNotMatchPartialPrefix() 210ede46466SAndreas Gohr { 211ede46466SAndreas Gohr // @foo should not match pages in foobar: namespace 212ede46466SAndreas Gohr $terms = [ 213ede46466SAndreas Gohr 'test' => $this->makeTerm('test', [ 214ede46466SAndreas Gohr 'foo:page1' => 1, 215ede46466SAndreas Gohr 'foobar:page2' => 2, 216ede46466SAndreas Gohr 'foo:sub:page3' => 3, 217ede46466SAndreas Gohr ]), 218ede46466SAndreas Gohr ]; 219ede46466SAndreas Gohr // RPN: test N+:foo AND 220ede46466SAndreas Gohr $rpn = ['W+:test', 'N+:foo', 'AND']; 221ede46466SAndreas Gohr 222ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 223ede46466SAndreas Gohr $result = $evaluator->evaluate(); 224ede46466SAndreas Gohr 225ede46466SAndreas Gohr // foobar:page2 must NOT match — only foo: prefix pages 226ede46466SAndreas Gohr $this->assertEquals(['foo:page1' => 1, 'foo:sub:page3' => 3], $result); 227ede46466SAndreas Gohr } 228ede46466SAndreas Gohr 229ede46466SAndreas Gohr // endregion 230ede46466SAndreas Gohr 231ede46466SAndreas Gohr // region Empty result cases 232ede46466SAndreas Gohr 233ede46466SAndreas Gohr public function testAndNoOverlap() 234ede46466SAndreas Gohr { 235ede46466SAndreas Gohr $terms = [ 236ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 1]), 237ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page2' => 1]), 238ede46466SAndreas Gohr ]; 239ede46466SAndreas Gohr $rpn = ['W+:foo', 'W+:bar', 'AND']; 240ede46466SAndreas Gohr 241ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 242ede46466SAndreas Gohr $result = $evaluator->evaluate(); 243ede46466SAndreas Gohr 244ede46466SAndreas Gohr $this->assertEquals([], $result); 245ede46466SAndreas Gohr } 246ede46466SAndreas Gohr 247ede46466SAndreas Gohr public function testNotRemovesAll() 248ede46466SAndreas Gohr { 249ede46466SAndreas Gohr $terms = [ 250ede46466SAndreas Gohr 'foo' => $this->makeTerm('foo', ['page1' => 1]), 251ede46466SAndreas Gohr 'bar' => $this->makeTerm('bar', ['page1' => 2]), 252ede46466SAndreas Gohr ]; 253ede46466SAndreas Gohr // foo -bar where bar covers all foo pages 254ede46466SAndreas Gohr $rpn = ['W+:foo', 'W-:bar', 'NOT', 'AND']; 255ede46466SAndreas Gohr 256ede46466SAndreas Gohr $evaluator = new QueryEvaluator($rpn, $terms); 257ede46466SAndreas Gohr $result = $evaluator->evaluate(); 258ede46466SAndreas Gohr 259ede46466SAndreas Gohr $this->assertEquals([], $result); 260ede46466SAndreas Gohr } 261ede46466SAndreas Gohr 262ede46466SAndreas Gohr public function testEmptyRpn() 263ede46466SAndreas Gohr { 264ede46466SAndreas Gohr $evaluator = new QueryEvaluator([], []); 265ede46466SAndreas Gohr $result = $evaluator->evaluate(); 266ede46466SAndreas Gohr 267ede46466SAndreas Gohr $this->assertEquals([], $result); 268ede46466SAndreas Gohr } 269ede46466SAndreas Gohr 270ede46466SAndreas Gohr // endregion 271ede46466SAndreas Gohr} 272