<?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
*
* This file contains unit tests of the IndexDocumentBundleTable class
*
* @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\library as L;
use seekquarry\yioop\models\Model;
use seekquarry\yioop\library\CrawlConstants as CC;
use seekquarry\yioop\library\IndexDocumentBundle;
use seekquarry\yioop\library\IndexManager;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\library\index_bundle_iterators\WordIterator;
/**
* Tests the functionality of the WordIterator class used to iterate over
* documents in an IndexDocumentBundle containing a term.
*/
class WordIteratorTest extends UnitTest
{
/**
* Name of IndexDocumentBundle used to store test documents
*/
const TEST_BUNDLE = "IndexData200000000";
/**
* Prefix of folders for index manager test
*/
const TEST_DIR = __DIR__ . '/test_files/word_iterator_test';
/**
* IndexDocumentBundle used to store test documents
* @var object
*/
public $index_archive;
/**
* Sets up test dir for document bunble then instantiates
* IndexDocumentBundle
*/
public function setUp()
{
$test_bundle = self::TEST_BUNDLE;
if (!file_exists(self::TEST_DIR)) {
mkdir(self::TEST_DIR);
}
$this->index_archive = new IndexDocumentBundle(self::TEST_DIR .
"/$test_bundle", false, "TestBundle", 100, 101);
}
/**
* Deletes test directory and test document bundle
*/
public function tearDown()
{
$model = new Model();
$model->db->unlinkRecursive(self::TEST_DIR);
}
/**
* Sets of a IndexDocumentBundle storing 10 documents. The word 'be'
* appears in every document and the words or1, or2, ..., or10 each
* appear in 1 document. The test then checks the number of documents
* returns by the WordIterator's currentDocsWithWord method to see if
* this is the case.
*/
public function findTermTestCase()
{
IndexManager::clearCache();
$index_archive = $this->index_archive;
$dictionary = $index_archive->dictionary;
$keys = [];
for ($i = 0; $i < 10; $i++) {
$keys[$i] = $this->docidFromIntKeys($i, $i, $i);
$docs[] = [
CC::DOC_ID => $keys[$i],
CC::SUMMARY =>
[
CC::DESCRIPTION => "to$i be or$i not$i to$i be...",
CC::HASH => str_pad("$i", 8, "0", STR_PAD_LEFT),
CC::TITLE => "Some$i Shakespeare$i Play$i",
CC::URL => "https://www.somewhere$i.com/"
],
CC::PAGE => "Page $i",
];
}
$num_docs = count($docs);
$index_archive->addPages($docs, $num_docs);
$index_archive->updateDictionary();
$index_archive->forceSave();
$word_iterator = new WordIterator(L\canonicalTerm("be"),
self::TEST_DIR . "/" . self::TEST_BUNDLE, true, null, 10);
$current_doc_with_word = $word_iterator->currentDocsWithWord();
$current_doc_with_word = (is_array($current_doc_with_word)) ?
$current_doc_with_word : [];
$this->assertEqual(count($current_doc_with_word), 10,
"Number of occurrences of 'be' correct");
for ($i = 0; $i < 10; $i++) {
$word_iterator = new WordIterator(L\canonicalTerm("or$i"),
self::TEST_DIR . "/". self::TEST_BUNDLE, true, null, 10);
$current_doc_with_word = $word_iterator->currentDocsWithWord();
$current_doc_with_word = (is_array($current_doc_with_word)) ?
$current_doc_with_word : [];
$this->assertEqual(count($current_doc_with_word), 1,
"Number of occurrences of 'or$i' correct");
}
}
/**
* Auxiliary method used to create a document id for a test document
* @param int $i_hash_url an integer that is supposed to be used
* to correspond to the hash of a url component of a doc_id
* @param int $j_hash_page an integer that is supposed to be used
* to correspond to the hash of a web page component of a doc_id
* @param int $k_hash_host an integer that is supposed to be used
* to correspond to the hash of a url's host component of a doc_id
* @param bool $is_doc whether to make a doc_id for a link to doc item or
* for a document
* @return string doc_id to associate with that integer
*/
protected function docidFromIntKeys($i_hash_url, $j_hash_page,
$k_hash_host, $is_doc = true)
{
$doc_or_link = ($is_doc) ? "d" : "l";
return str_pad("$i_hash_url", 8, "0", STR_PAD_LEFT) .
str_pad("$j_hash_page", 8, "0", STR_PAD_LEFT) .
$doc_or_link .
str_pad("$k_hash_host", 7, "0", STR_PAD_LEFT);
}
/**
* A positions read whose length is past the maximum readable posting
* length is treated as corrupt and skipped, reading nothing, while a
* length within bounds reads and decodes the positions normally. This
* guards the positions read the way the posting-string read is guarded,
* so a corrupt length cannot make the server allocate a huge block.
*/
public function positionsReadCapTestCase()
{
$positions = [3, 7, 11];
$encoded = L\encodePositionList($positions);
$positions_file = C\WORK_DIRECTORY . "/positions_cap_test.bin";
file_put_contents($positions_file, $encoded);
$handle = fopen($positions_file, "r");
$iterator = (new \ReflectionClass(WordIterator::class))->
newInstanceWithoutConstructor();
$within = $iterator->getPositionsList(["POSITIONS_LEN" =>
strlen($encoded), "POSITIONS_OFFSET" => 0,
"FREQUENCY" => count($positions)], strlen($encoded) + 1000,
$handle);
$this->assertEqual($positions, $within,
"a positions length within bounds reads and decodes");
$before = memory_get_usage();
$skipped = $iterator->getPositionsList(["POSITIONS_LEN" =>
C\MAX_POSTING_READ_LEN + 1, "POSITIONS_OFFSET" => 0,
"FREQUENCY" => 1], C\MAX_POSTING_READ_LEN * 4, $handle);
$after = memory_get_usage();
$this->assertEqual([], $skipped,
"a positions length past the cap is skipped");
$this->assertTrue($after - $before < C\MAX_POSTING_READ_LEN,
"the skipped read allocates nothing large");
fclose($handle);
unlink($positions_file);
}
}