<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\tests;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\models\PhraseModel;
use seekquarry\yioop\library\UnitTest;
/**
* PhraseModelTest holds the unit tests for PhraseModel, covering how the
* results of a query are reordered once their summaries are fetched.
*
* @author Chris Pollett
*/
class PhraseModelTest extends UnitTest
{
/**
* setUp makes a PhraseModel that opens no database connection
*/
public function setUp()
{
$this->test_objects['MODEL'] = new PhraseModel(C\DB_NAME, false);
}
/**
* tearDown drops the model made by setUp
*/
public function tearDown()
{
$this->test_objects = [];
}
/**
* exactMatchMultipliesTheRelevanceTestCase checks the case of a query
* for anime, which also finds pages about animals, as the two words
* have the same stem. A result whose title or summary holds the word
* anime, in any case, has its relevance multiplied by
* EXACT_MATCH_FACTOR; one holding only animal keeps its score.
*/
public function exactMatchMultipliesTheRelevanceTestCase()
{
$fields = [PhraseModel::TITLE, PhraseModel::DESCRIPTION,
PhraseModel::RELEVANCE, PhraseModel::SCORE];
$pages = [
array_combine($fields, ["Animal Liberation Front-Line",
"Animal Liberation Front", 4.0, 11.0]),
array_combine($fields, ["Watch Free Movies and TV Shows Online",
"Adult <b>Anime</b>", 3.0, 10.0]),
];
$this->test_objects['MODEL']->addExactMatchScore($pages, ["anime"]);
$factor = PhraseModel::EXACT_MATCH_FACTOR;
$this->assertEqual(array_column($pages, PhraseModel::SCORE),
[7.0 + 3.0 * $factor, 11.0],
"The exact match has its relevance multiplied");
}
/**
* exactMatchIgnoresDiacriticsTestCase checks that a query word typed
* without diacritics, jose, counts as an exact match for a title that
* writes it with them, San José State University, as the index files
* both forms under one term.
*/
public function exactMatchIgnoresDiacriticsTestCase()
{
$fields = [PhraseModel::TITLE, PhraseModel::DESCRIPTION,
PhraseModel::RELEVANCE, PhraseModel::SCORE];
$pages = [array_combine($fields, ["San José State University",
"", 4.0, 10.0])];
$this->test_objects['MODEL']->addExactMatchScore($pages, ["jose"]);
$factor = PhraseModel::EXACT_MATCH_FACTOR;
$this->assertEqual($pages[0][PhraseModel::SCORE],
6.0 + 4.0 * $factor, "jose matches José exactly");
}
/**
* unspacedWordFoundAndLeftOutWordIgnoredTestCase checks that the word
* 动漫, anime, is found inside a title written without spaces between
* its words, and that a word left out of the query with a minus is not
* looked for. Of the two words looked for, only one is in the title,
* so half the relevance is multiplied.
*/
public function unspacedWordFoundAndLeftOutWordIgnoredTestCase()
{
$fields = [PhraseModel::TITLE, PhraseModel::DESCRIPTION,
PhraseModel::RELEVANCE, PhraseModel::SCORE];
$pages = [
array_combine($fields, ["cats only", "", 2.0, 3.0]),
array_combine($fields, ["最新动漫新闻", "", 2.0, 3.0]),
];
$this->test_objects['MODEL']->addExactMatchScore($pages,
["动漫", "新番", "-cats"]);
$factor = PhraseModel::EXACT_MATCH_FACTOR;
$this->assertEqual(array_column($pages, PhraseModel::SCORE),
[3.0 + ($factor - 1) * 2.0 / 2, 3.0],
"Only the Chinese title has its relevance multiplied");
}
/**
* xResults gives ten results for the query x, with the titles,
* descriptions and scores the search site yioop.com (Chris Pollett's
* site) returned for them on 2026-10-13, in score order.
*
* @return array the results, as getSummariesByHash holds them
*/
public function xResults()
{
$fields = [PhraseModel::URL, PhraseModel::TITLE,
PhraseModel::DESCRIPTION, PhraseModel::SCORE];
$rows = [
["https://www.dictionary.com/games/word-finder/" .
"words-with-the-letter-x",
"Words With the <b>Letter</b> <b>X</b> | Dictionary.com",
"Words With the <b>Letter</b> <b>X</b> for Wordle, " .
"Scrabble, Words with ... Featured Games", 38.049],
["https://www.foxnews.com/games/lil-snack-word-swap",
"SWAP Mini Game | Fox News <b>x</b> Lil Snack Addictive " .
"Mini Games | Fox News", "Swap the <b>letter</b> tiles to " .
"solve each clue in this addictive <b>letter</b> swap " .
"mini game!", 32.385],
["https://www.dictionary.com/games/word-finder/" .
"words-that-start-with-x",
"Words That Start with <b>X</b> | Dictionary.com",
"That Start with <b>X</b> for Wordle, Scrabble, Words with " .
"... Featured Games ... 1-<b>Letter</b> Words.", 31.395],
["https://www.dictionary.com/games/word-finder/" .
"words-that-end-with-x",
"Words That End with <b>X</b> | Dictionary.com",
"Words That End with <b>X</b> for Wordle, Scrabble, Words " .
"with ... Featured Games", 30.576],
["https://x.com/", "<b>X</b>. It’s what’s happening / <b>X</b>",
"live commentary. <b>X</b>. It’s what’s happening ... 2026 " .
"<b>X</b> Corp", 27.672],
["http://www.symphonyx.com/",
"Symphony <b>X</b> Official Website",
"Official Symphony <b>X</b> band website", 27.154],
["https://titlexwines.com/", "Title <b>X</b> | A Convergence of " .
"Wine & Art – TITLE <b>X</b> Wines", "Title <b>X</b> unites " .
"world-renowned artist Mister ... your access.", 27.077],
["https://www.lilnasx.com/", "LIL NAS <b>X</b>", "LIL NAS " .
"<b>X</b>. Lil Nas <b>X</b> Store Listen Watch. Out", 26.605],
["https://directorxfilms.com/", "Home | Director <b>X</b>",
"Home | Director <b>X</b>. Robyn Hood (Trailer). Global TV.",
26.128],
["https://www.xjapan.com/", "<b>X</b> Japan Official Website",
"<b>X</b> Japan is the most successful rock band ... outside " .
"of Japan.", 26.040],
];
return array_map(fn($row) => array_combine($fields, $row), $rows);
}
/**
* diversifyMovesResultsLikeOnesAboveDownTestCase gives diversify the ten
* results yioop.com gave for x, where three dictionary.com pages on
* words with the letter x stand at places 1, 3 and 4 and x.com at
* place 5. Each of the second and third dictionary.com pages shares
* most of its words with the first, so they move below x.com. With
* lambda set to 1, relevance alone counts and the order is kept.
*/
public function diversifyMovesResultsLikeOnesAboveDownTestCase()
{
$pages = $this->xResults();
$urls = array_column($pages, PhraseModel::URL);
$this->test_objects['MODEL']->diversify($pages, ["x"], 30, 1);
$this->assertEqual($urls, array_column($pages, PhraseModel::URL),
"with lambda 1 the order is kept");
$this->test_objects['MODEL']->diversify($pages, ["x"], 30, 0.7);
$places = array_flip(array_column($pages, PhraseModel::URL));
$this->assertTrue($places["https://x.com/"] <
$places[$urls[2]] && $places["https://x.com/"] < $places[$urls[3]],
"x.com moves above the second and third dictionary.com pages");
$this->assertEqual($urls[0], $pages[0][PhraseModel::URL],
"the top result stays first");
}
}