Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Atlas Search example #1147

Merged
merged 2 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions examples/atlas-search.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

/**
* This example demonstrates how to create an Atlas Search index and perform a search query.
* It requires a MongoDB Atlas M10+ cluster with Sample Dataset loaded.
*
* Use the MONGODB_URI environment variable to specify the connection string from the Atlas UI.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of a comment header explaining the purpose of the example. This might be a good place to come up with some header copypasta that notes supported environment variables and/or CLI arguments (down the line, not in this PR).


declare(strict_types=1);

namespace MongoDB\Examples;

use Closure;
use MongoDB\Client;
use RuntimeException;

use function define;
use function getenv;
use function hrtime;
use function iterator_to_array;
use function sleep;
use function str_contains;

require __DIR__ . '/../vendor/autoload.php';

$uri = getenv('MONGODB_URI');
jmikola marked this conversation as resolved.
Show resolved Hide resolved
if (! $uri || ! str_contains($uri, '.mongodb.net')) {
echo 'This example requires a MongoDB Atlas cluster.', "\n";
echo 'Make sure you set the MONGODB_URI environment variable.', "\n";
exit(1);
}

// Atlas Search index management operations are asynchronous.
// They usually take less than 5 minutes to complete.
define('WAIT_TIMEOUT_SEC', 300);

// The sample dataset is loaded into the "sample_airbnb.listingsAndReviews" collection.
$databaseName = getenv('MONGODB_DATABASE') ?: 'sample_airbnb';
$collectionName = getenv('MONGODB_COLLECTION') ?: 'listingsAndReviews';

$client = new Client($uri);
$collection = $client->selectCollection($databaseName, $collectionName);

$count = $collection->estimatedDocumentCount();
if ($count === 0) {
echo 'This example requires the "', $databaseName, '" database with the "', $collectionName, '" collection.', "\n";
echo 'Load the sample dataset in your MongoDB Atlas cluster before running this example:', "\n";
echo ' https://www.mongodb.com/docs/atlas/sample-data/', "\n";
exit(1);
}

// Delete the index if it already exists
$indexes = iterator_to_array($collection->listSearchIndexes());
foreach ($indexes as $index) {
if ($index->name === 'default') {
echo "The index already exists. Dropping it.\n";
$collection->dropSearchIndex($index->name);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is idempotent, I can remove surrounding loop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact no, the error for NamespaceNotFound is silenced, not the error for IndexNotFound.
https://github.com/mongodb/specifications/blob/master/source/index-management/index-management.rst#namespacenotfound-errors


// Wait for the index to be deleted.
wait(function () use ($collection) {
echo '.';
foreach ($collection->listSearchIndexes() as $index) {
if ($index->name === 'default') {
return false;
}
}

return true;
});
}
}

// Create the search index
echo "\nCreating the index.\n";
$collection->createSearchIndex(
/* The index definition requires a mapping
* See: https://www.mongodb.com/docs/atlas/atlas-search/define-field-mappings/ */
['mappings' => ['dynamic' => true]],
// "default" is the default index name, this config can be omitted.
['name' => 'default'],
);

// Wait for the index to be ready.
wait(function () use ($collection) {
echo '.';
foreach ($collection->listSearchIndexes() as $index) {
if ($index->name === 'default') {
return $index->queryable;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a boolean, but I could not document it in phpdoc. #1097 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there's no Psalm error here. Is it worth casting this to a boolean and using bool return type hint for the callback? OK to leave this as-is if it doesn't matter.

}
}

return false;
});

// Perform a text search
echo "\n", 'Performing a text search...', "\n";
$results = $collection->aggregate([
jmikola marked this conversation as resolved.
Show resolved Hide resolved
[
'$search' => [
'index' => 'default',
'text' => [
'query' => 'view beach ocean',
'path' => ['name'],
],
],
],
['$project' => ['name' => 1, 'description' => 1]],
['$limit' => 10],
])->toArray();

foreach ($results as $document) {
echo ' - ', $document['name'], "\n";
}

echo "\n", 'Enjoy MongoDB Atlas Search!', "\n\n";

/**
* This function waits until the callback returns true or the timeout is reached.
*/
function wait(Closure $callback): void
{
$timeout = hrtime()[0] + WAIT_TIMEOUT_SEC;
while (hrtime()[0] < $timeout) {
if ($callback()) {
return;
}

sleep(5);
}

throw new RuntimeException('Time out');
}
24 changes: 24 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@
<code><![CDATA[$clientEncryption->decrypt($document->encryptedField)]]></code>
</MixedArgument>
</file>
<file src="examples/atlas-search.php">
<MixedArgument>
<code><![CDATA[$document['name']]]></code>
<code><![CDATA[$index->name]]></code>
</MixedArgument>
<MixedArrayAccess>
<code><![CDATA[$document['name']]]></code>
</MixedArrayAccess>
<MixedAssignment>
<code>$document</code>
<code>$index</code>
<code>$index</code>
<code>$index</code>
</MixedAssignment>
<MixedPropertyFetch>
<code><![CDATA[$index->name]]></code>
<code><![CDATA[$index->name]]></code>
<code><![CDATA[$index->name]]></code>
<code><![CDATA[$index->queryable]]></code>
</MixedPropertyFetch>
<PossiblyFalseArgument>
<code>$uri</code>
</PossiblyFalseArgument>
</file>
<file src="examples/persistable.php">
<LessSpecificReturnStatement>
<code><![CDATA[(object) [
Expand Down
54 changes: 54 additions & 0 deletions tests/ExamplesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

use Generator;

use function bin2hex;
use function getenv;
use function putenv;
use function random_bytes;
use function sprintf;

/** @runTestsInSeparateProcesses */
final class ExamplesTest extends FunctionalTestCase
{
Expand Down Expand Up @@ -179,6 +185,54 @@ public static function provideExamples(): Generator
];
}

/**
* MongoDB Atlas Search example requires a MongoDB Atlas M10+ cluster with MongoDB 7.0+
* Tips for insiders: if using a cloud-dev server, append ".mongodb.net" to the MONGODB_URI.
*
* @group atlas
*/
GromNaN marked this conversation as resolved.
Show resolved Hide resolved
public function testAtlasSearch(): void
{
$uri = getenv('MONGODB_URI') ?? '';
if (! self::isAtlas($uri)) {
$this->markTestSkipped('Atlas Search examples are only supported on MongoDB Atlas');
}

$this->skipIfServerVersion('<', '7.0', 'Atlas Search examples require MongoDB 7.0 or later');

// Generate random collection name to avoid conflicts with consecutive runs as the index creation is asynchronous
$collectionName = sprintf('%s.%s', $this->getCollectionName(), bin2hex(random_bytes(5)));
$databaseName = $this->getDatabaseName();
$collection = $this->createCollection($databaseName, $collectionName);
$collection->insertMany([
['name' => 'Ribeira Charming Duplex'],
['name' => 'Ocean View Bondi Beach'],
['name' => 'Luxury ocean view Beach Villa 622'],
['name' => 'Ocean & Beach View Condo WBR H204'],
['name' => 'Bondi Beach Spacious Studio With Ocean View'],
['name' => 'New York City - Upper West Side Apt'],
]);
putenv(sprintf('MONGODB_DATABASE=%s', $databaseName));
putenv(sprintf('MONGODB_COLLECTION=%s', $collectionName));

$expectedOutput = <<<'OUTPUT'

Creating the index.
%s
Performing a text search...
- Ocean View Bondi Beach
- Luxury ocean view Beach Villa 622
- Ocean & Beach View Condo WBR H204
- Bondi Beach Spacious Studio With Ocean View

Enjoy MongoDB Atlas Search!


OUTPUT;

$this->assertExampleOutput(__DIR__ . '/../examples/atlas-search.php', $expectedOutput);
}

public function testChangeStream(): void
{
$this->skipIfChangeStreamIsNotSupported();
Expand Down