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

Upgrade from deprecated app-search package to enterprise-search #70

Closed
wants to merge 3 commits into from

Conversation

jakxnz
Copy link
Contributor

@jakxnz jakxnz commented Jun 5, 2022

Raising this draft for Travis CI perks.

I am conscious that #69 exists.

… PHPUnit 9.5

* Enhanced SearchAdmin::getEditForm()
… PHPUnit 9.5

* Upgraded elastic/app-search to elastic/enterprise-search
@chrispenny
Copy link
Collaborator

chrispenny commented Jun 7, 2022

Trying as best I can to share my learnings and difficulties below.

Elastic Client contract has changed

  • As part of our EnterpriseSearchService we instantiate an Elastic Client.
  • This Elastic Client used to contain all of the methods that we interact with (EG: addDocuments()). This is no longer the case
  • The Elastic Client now has an extra level of abstraction.
  • We now need to access $client->appSearch() or $client->enterpriseSeearch() before we are able to access the original methods.
  • Furthermore, the methods used to take in basic params, whereas they now need us to instantiate a Request and pass that through.

Practical example

Before:

$this->getClient()->indexDocuments(static::environmentizeIndex($indexName), $docsToAdd);

After:

$this->getClient()->appSearch()
    ->indexDocuments(new IndexDocuments(static::environmentizeIndex($indexName), $docsToAdd))
    ->asArray();

Most of these should be updated already as part of my commit (though I've only validated the configure method so far)

At first, this doesn't seem like a big deal, and to be honest, for the practical application it isn't a big deal. However, because of the way that our unit tests are mocked, I think this is a significant paradigm shift that is going to completely invalidate the way that we create our test mocks.

I think our Unit Test paradigm is going to have to switch significantly

I apologise in advanced if I don't explain this very well... I barely understand it all myself.

EnterpriseSearchServiceTest currently mocks out the Elastic Client using SearchServiceTest::mockClient();. This method is creating a MockBuilder that mocks particular methods, such as indexDocuments, deleteDocuments, etc.

The problem is... These methods are no longer present in the Elastic Client like they were before, they're now in that next level of abstraction (appSearch).

I don't think we can "mock" appSearch methods using this MockBuilder.

I also don't know if we actually want to use the MockBuilder even if we could? From what I can tell, this mocks the response from a method (EG: it mocks the response from the indexDocuments() method?), as apposed to what (I think) we want to do, which is to instead mock the response from the API (wherever it happens).

So how do we mock the API responses

Yea... I'm struggling with this one a bit. Here's what I have so far. Untested, but I think it'd work.

When we instantiate an Elastic Client, we have the ability to pass in some $config. Any time we access Client::appSearch() or Client::enterpriseSearch() it instantiates (what they call) a TransportBuilder and it passes this builder the $config that we have for this Client.

Here is the method where the TransportBuilder is instantiated. We cannot inject over this, however the initBuilder() and build() methods both have some interesting/useful stuff.

private function buildTransport(array $config): Transport
{
    $builder = TransportBuilder::create();
    $this->initBuilder($builder, $config); // important
    $transport = $builder->build(); // important
    $this->initTransport($transport, $config);

    return $transport;
}

(Removed some stuff to focus in on what is important).

private function initBuilder(TransportBuilder $builder, array $config): void
{
    ...
    foreach ($config as $key => $value) {
        $set = 'set' . ucfirst($key);
        if (method_exists($builder, $set)) {
            $builder->$set($value);
            unset($config[$key]);
        }
    }
}

Ok, so it looks through all of our $config and checks to see if there are matching methods on the TransportBuilder. The key method that does exist on the TransportBuilder is setClient().

public function setClient(ClientInterface $client): self
{
    $this->client = $client;
    return $this;
}

And then when build() is called...

public function build(): Transport
{
    $connectionPool = $this->connectionPool ?? new SimpleConnectionPool;
    $connectionPool->setHosts($this->hosts);

    $transport = new Transport(
        $this->client ?? new GuzzleClient,
        $connectionPool,
        $this->logger ?? new NullLogger
    );
    return $transport;
}

Yaaaay! There's our Transport, and if we have set a client (say, to a GuzzleClient that has a MockHandler and HandlerStack already instantiated), then we should be able to mock responses that are being called through our service???

Ok, so practical example?

Untested, but I think it'd look something like this:

class SharepointCoordinatorTest extends SapphireTest
{

    protected ?MockHandler $mock;

    protected EnterpriseSearchService $searchService;

    protected function setUp(): void
    {
        parent::setUp();

        // Set up a mock handler/client so that we can feed in mock responses that we expected to get from the API
        $this->mock = new MockHandler([]);
        $handler = HandlerStack::create($this->mock);
        $client = new GuzzleClient(['handler' => $handler]);

        $config = [
            'host' => 'https://api.elastic.com',
            'app-search' => [
                'token' => 'test-token',
            ],
            'Client' => $client,
        ];

        $elasticClient = new ElasticClient($config);
        $indexConfiguration = Injector::inst()->get(IndexConfiguration::class, true, 'dev-test');
        $documentBuilder = Injector::inst()->get(DocumentBuilder::class);

        $this->searchService = EnterpriseSearchService::create($elasticClient, $indexConfiguration, $documentBuilder);
    }

    public function testSomething(): void
    {
        $headers = [
            'Content-Type' => 'application/json;charset=utf-8',
        ];
        $body = json_encode([]);

        $this->mock->append(new Response(200, $headers, $body));

        // Start invoking methods that trigger API calls, and they should respond with the mock above
        // This is a "stack" as well, so you can add more than one response if you need to. They get "popped"
        // in the order that they are added
    }

}

@jakxnz jakxnz changed the title Added support for users installing fresh on PHP 8 infrastructure with PHPUnit 9.5 Upgrade from deprecated app-search package to enterprise-search Jun 9, 2022
@jakxnz
Copy link
Contributor Author

jakxnz commented Jun 9, 2022

After a chat with @chrispenny , we designated the direction of this PR's changes to be earmarked as a silverstripe-search-service@2.0, which takes us from abandoned dependencies to the latest alternative to those dependencies

e.g from elastic/app-search@7.6 to elastic/enterprise-search@^7.17

@chrispenny
Copy link
Collaborator

Closed in favour of #76

@chrispenny chrispenny closed this Aug 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants