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

feat(search-client): Add support for Custom Search Clients #2894

Merged
merged 24 commits into from
Apr 26, 2018

Conversation

francoischalifour
Copy link
Member

@francoischalifour francoischalifour commented Apr 17, 2018

This PR adds support for the searchClient parameter. This allows any InstantSearch user to plug their own search client (including a custom Algolia backend).

Summary

The goal is to add support for this new feature but to still make InstantSearch work as it works right now. This is meant to be released in the next minor version. Checks have been made to make sure both APIs (current and new) don't collide.

// With the Algolia JS client
instantsearch({
  indexName: 'concert_events_instantsearchjs',
  searchClient: algoliasearch('latency', '059c79ddd276568e990286944276464a'),
});

// With a custom search client
instantsearch({
  indexName: 'concert_events_instantsearchjs',
  searchClient: {
    search(requests) {
      // perform requests...
      return response;
    },
  },
});

Features

  • Add searchClient option to plug a custom search client (other than algoliasearch)
  • Deprecate createAlgoliaClient in favor of searchClient
  • Add tests to make sure the current and new API don't collide

Releasing strategy

  • the next minor version will embed algoliasearch, although we ship this new searchClient parameter
  • the next major version will get rid of algoliasearch that will need to be imported beforehand and plugged to searchClient

Demo

You can check the Calendar Widget demo using InstantSearch with searchClient: https://instantsearch-search-client.now.sh


Let me know if I've forgotten any parameter collision 🙌

@algobot
Copy link
Contributor

algobot commented Apr 17, 2018

Deploy preview for algolia-instantsearch ready!

Built with commit fa0ca7f

https://deploy-preview-2894--algolia-instantsearch.netlify.com

Copy link
Contributor

@Haroenv Haroenv left a comment

Choose a reason for hiding this comment

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

I like this feature!

const client = createAlgoliaClient(algoliasearch, appId, apiKey);
client.addAlgoliaAgent(`instantsearch.js ${version}`);
// eslint-disable-next-line prefer-rest-params
const [THROWAWAY_PARAMS] = arguments;
Copy link
Contributor

Choose a reason for hiding this comment

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

this line is a bit odd, what about giving the args to this function this name, and destructure after this line?

Copy link
Member Author

Choose a reason for hiding this comment

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

It would indeed be cleaner. I wanted to change the fewest possible number of lines since this is meant to be thrown away soon.

Is it still worth it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe leave the code as-is but add a comment that this is the first argument, which will be removed? Not totally sure what to do here

search.start();

expect(search.helper.state.query).toBe('');
expect(searchClientSpy.search).toHaveBeenCalledTimes(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

also useful to assert with what it's been called?

Copy link
Member Author

Choose a reason for hiding this comment

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

For your information this test was taken from: https://github.com/algolia/instantsearch.js/blob/develop/src/lib/__tests__/InstantSearch-test-2.js#L11

It's called with this:

[
  {
    "indexName": "",
    "params": { "facets": [], "page": 0, "query": "test", "tagFilters": "" }
  }
]

Is it worth creating a snapshot for this?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd go with a regular test here rather than a snapshot one, but not sure if it needs to be tested if already asserted enough in other spots

Copy link
Contributor

Choose a reason for hiding this comment

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

Good call on testing the search() parameters here 👍 Considering the black box nature of the parameters, I'd go for a snapshot. Either would work (it's just more maintainance to not use snapshots)

it('and nothing else', () => {
expect(() => {
new InstantSearch({
searchClient: {},
Copy link
Contributor

Choose a reason for hiding this comment

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

Miss indexName

}).toThrow();
});

it('and indexname, appId, apiKey', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe not necessary.

search.start();

expect(search.helper.state.query).toBe('');
expect(searchClientSpy.search).toHaveBeenCalledTimes(1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Good call on testing the search() parameters here 👍 Considering the black box nature of the parameters, I'd go for a snapshot. Either would work (it's just more maintainance to not use snapshots)

search.start();

expect(searchFunctionSpy).toHaveBeenCalledTimes(1);
expect(search.helper.state.query).toBe('test');
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd use the same strategy as for the previous test

@bobylito bobylito changed the base branch from develop to feat/2.8 April 18, 2018 09:36
}

const client = {
__proto__: Object.getPrototypeOf(searchClient),
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that this is standardised properly.

What about doing instead:

if (typeof searchClient.clearCache === 'function') {
  searchClient.clearCache = function() {};
}

if (typeof searchClient.addAlgoliaAgent === 'function') {
  searchClient.addAlgoliaAgent = function() {};
}

or doing this check in the places in InstantSearch those two functions are used?

Copy link
Contributor

Choose a reason for hiding this comment

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

or better:

const client = Object.create(person);

client.addAlgoliaAgent = client.addAlgoliaAgent || function() {};
client.clearCache = client.clearCache || function() {};

Copy link
Contributor

Choose a reason for hiding this comment

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

see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto for more info on why __proto__ probably shouldn't be used. Object.create seems to be supported on all browsers we support 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

I like your second solution, thanks! I needed __proto__ because algoliasearch relies on it. We come back to the initial solution but with Object.create().

Copy link
Contributor

Choose a reason for hiding this comment

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

I like your second solution, thanks! I needed proto because algoliasearch relies on it.

Quick question, since this is a custom search client we have here, why do we need to have __proto or Object.create(). Which part of the code will expect this object to be made this way?

Copy link
Contributor

Choose a reason for hiding this comment

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

What if you just tested if methods you need are existing and if that's the case then do not even care about creating a new object (most probably this is already a good client, ours). And only create a new object if those methods are missing

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, my preferred option would be to check if addAlgoliaAgent exists when we are using it, rather than this workaround of making it always callable

Copy link
Member Author

Choose a reason for hiding this comment

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

We could have done this:

const client = searchClient;

But this kind of assignation is a reference in JavaScript. That means that if ever we do client.hello = true, it will mutate searchClient. I think we shouldn't risk it. Object.create() creates a deep clone of the object.

We could also have done this:

const client = { ...searchClient };

But this doesn't copy the prototype, on which algoliasearch relies (it is not a simple object). We'd lose the reference to client.search() (becoming undefined).

Therefore, I think that Object.create() is the most appropriate thing to do anyway.

Does that answer your question? Do you agree with this decision?

Copy link
Contributor

@vvo vvo Apr 19, 2018

Choose a reason for hiding this comment

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

Do you agree with this decision?

Yes and thanks for the explanations, it might be good to add a small comment saying "We don't want to use the same reference so we clone the client no matter if it already have the necessary methods"

if (options.createAlgoliaClient) {
// eslint-disable-next-line no-console
console.warn(
'InstantSearch.js: `createAlgoliaClient` option is deprecated and will be removed in the next major version. Please use `searchClient` instead.'
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a link to the documentation that explains how to do this switch? Always good to have more than just an error message, but an actual actionable link

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I'm working on a guide "Preparing for v3" that I will link in this warning message.

src/lib/main.js Outdated
@@ -110,6 +116,37 @@ import * as stateMappings from './stateMappings/index.js';
* @property {number} [stalledSearchDelay=200] Time before a search is considered stalled.
* @property {RoutingOptions} [routing] the router configuration used to save the UI State into the URL or
* any client side persistence.
* @property {SearchClient} [searchClient] The search client to plug to instantsearch.js. You should start updating with this
* syntax to ease the migration to InstantSearch 3.
Copy link
Member Author

Choose a reason for hiding this comment

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

We could link to the "Preparing for v3" guide once it's published.

src/lib/main.js Outdated
* @property {function} [createAlgoliaClient] _Deprecated in favor of [`searchClient`](instantsearch.html#struct-InstantSearchOptions-searchClient)._<br>
* Allows you to provide your own algolia client instead of the one instantiated internally by instantsearch.js.
* Useful in situations where you need to setup complex mechanism on the client or if you need to share it easily.
* <br>Usage:
Copy link
Contributor

Choose a reason for hiding this comment

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

can you also leave a newline instead of br?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, didn't realize we could.

Copy link
Contributor

@Haroenv Haroenv left a comment

Choose a reason for hiding this comment

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

LGTM if the guide is linked

* docs(guide): Add "Prepare for v3" guide
* feat(guide): Lower nav weight for routing guide
This makes the "Prepare for v3" guide last in the nav list.

* feat(menu): Add "Prepare for v3" guide in the menu
* docs: Add intro and change titles
* docs: Add "Deprecations"
* docs: Rephrase introduction
* docs: Move second part of intro in first section
@francoischalifour
Copy link
Member Author

}).toThrowErrorMatchingSnapshot();
});

it('should have default `addAlgoliaAgent()` and `clearCache()` methods', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

We'll get rid of this test once #2903 is merged.

@Haroenv
Copy link
Contributor

Haroenv commented Apr 25, 2018

@francoischalifour, in that case, is the object.create even still necessary?

@francoischalifour
Copy link
Member Author

Nope, I don't think so. It is our responsibility to never mutate the given client though.

@bobylito bobylito merged commit 5df3c74 into feat/2.8 Apr 26, 2018
@bobylito bobylito deleted the feat/search-client-support branch April 26, 2018 14:37
francoischalifour added a commit that referenced this pull request Apr 26, 2018
<a name=2.8.0-beta.0></a>
# [2.8.0-beta.0](v2.7.2...v2.8.0-beta.0) (2018-04-26)

### Features

* **search-client:** Add support for Universal Search Clients ([#2894](#2894)) ([5df3c74](5df3c74)), closes [#2905](#2905)
bobylito pushed a commit that referenced this pull request May 3, 2018
<a name=2.8.0-beta.1></a>
# [2.8.0-beta.1](v2.7.4...v2.8.0-beta.1) (2018-05-03)

### Features

* **search-client:** Add support for Universal Search Clients ([#2894](#2894)) ([5df3c74](5df3c74)), closes [#2905](#2905)
bobylito pushed a commit that referenced this pull request May 30, 2018
<a name=2.8.0></a>
# [2.8.0](v2.7.6...v2.8.0) (2018-05-30)

### Features

* **connectors:** add connectAutocomplete ([#2841](#2841)) ([4bec81e](4bec81e)), closes [/github.com//pull/2841#discussion_r188383882](https://github.com//github.com/algolia/instantsearch.js/pull/2841/issues/discussion_r188383882) [#2313](#2313)
* **search-client:** Add support for Universal Search Clients ([#2894](#2894)) ([5df3c74](5df3c74)), closes [#2905](#2905)
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.

5 participants