Using Typesense as your search provider for Laravel Scout and Instantsearch.

Typesense is a great open-source alternative for Algolia. Given that Algolia has a pretty expensive pricing package, it could be worth having a look to Typesense.

On top of that, it has already some integrations with famous packages like Instantsearch.js and Laravel Scout.

It started recently, so it’s documentation still lacks some examples. That is why I have written this post:

Using Typesense for Laravel

What is great is that there is a Laravel Scout package available: https://github.com/devloopsnet/laravel-scout-typesense-engine

Fixing the first Typesense errors with Laravel:

Most likely, you will receive errors when you use the default settings as described in the Typesense Laravel Scout package:

{"message": "Document's `id` field should be a string."}

That is solvable by converting the id to a string in the Searchable Array:

/**
* Get the indexable data array for the model.
*
* @return array
*/
public function toSearchableArray()
{
   $array = $this->toArray();
   // Typesense specific
   if (config('scout.driver') == 'typesensesearch') {
      $array['id'] = (string)$array['id']; $array['created_at'] = (integer)\Carbon::parse($array['created_at'])->timestamp;
   }
   return $array;
}

Note that I also converted the created_at datetime value to a timestamp integer, so that Typesense could easy sort on the integer value of the date.

Adding the Instantsearch.js Typesense Adapter to Laravel

You can follow the steps as described by the Typesense Adapter instructions. Also don’t forget to npm install algoliasearch and instantsearch.js .

Inserting in Laravel Mix

You can insert the following code in the webpack.mix.js file for Laravel Mix:

mix.extract(['algoliasearch', 'instantsearch.js', 'typesense-instantsearch-adapter'])
   .js(['resources/js/app.js'], 'public/js');

In your resources/js/app.js also get the imports ready:

import instantsearch from "instantsearch.js";
window.instantsearch = instantsearch;

import { searchBox, hits, index } from "instantsearch.js/es/widgets";
window.searchBox = searchBox;
window.hits = hits;
window.index = index;

import { connectAutocomplete } from 'instantsearch.js/es/connectors';
window.connectAutocomplete = connectAutocomplete;

import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
window.TypesenseInstantSearchAdapter = TypesenseInstantSearchAdapter;

Note that I have also included index and connectAutocomplete which are required for the Autocomplete Widget. If you are not planning to use those, you can leave them out.

Run Laravel Mix with (also use npm install if Mix has never been initialized):

npm run dev

In your main layout blade file add the generated mix files:

<!-- Scripts -->
<script src="{{ mix('/js/manifest.js') }}"></script>
<script src="{{ mix('/js/vendor.js') }}"></script>
<script src="{{ mix('/js/app.js') }}"></script>

Then you can simply insert the hits and searchbox:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/reset-min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/algolia-min.css">


<div id="hits"></div>
<div id="searchbox"></div>

<script>
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "{{ env('TYPESENSE_SEARCH_KEY') }}", // Be sure to use the search-only-api-key
    nodes: [
      {
        host: "{{ env('TYPESENSE_HOST') }}",
        port: "443",
        protocol: "https"
      }
    ]
  },
  // The following parameters are directly passed to Typesense's search API endpoint.
  //  So you can pass any parameters supported by the search endpoint below. 
  //  queryBy is required. 
  additionalSearchParameters: {
    queryBy: "name"
  }
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const search = instantsearch({
  searchClient,
  indexName: "products"
});

// Create the custom widget
const customAutocomplete = connectAutocomplete(
  renderAutocomplete
);

// Instantiate the custom widget
search.addWidgets([
  index({ searchClient, indexName: 'autolandings' }),

  customAutocomplete({
    container: document.querySelector('#autocomplete'),
  })
]);

search.start();
</script>

I’ve added the CSS theming of instantsearch.js to give it some basic styling. This one does not seem to work for Autocomplete yet. You have to fully style this one yourself (as far as I know).

How to use Instantsearch.js Autocomplete with Typesense

Typesense wrote an adapter that works with Instantsearch.js for Algolia. It has some basic examples to use the hits and searchBox component.

In some cases you would like to extend this functionality. For example, if you would like to use the Instantsearch.js autocomplete functionality you have to write additional code. To make a basic autocomplete functionality with multiple indices you can use this code:

Using multiple indices and Autocomplete for Instantsearch.js

In this example I have copied the code from Algolia’s Instantsearch.js Autocomplete website and integrated the Typesense adapter:

<div id="autocomplete"></div>

<script>
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "{{ env('TYPESENSE_SEARCH_KEY') }}", // Be sure to use the search-only-api-key
    nodes: [
      {
        host: "{{ env('TYPESENSE_HOST') }}",
        port: "443",
        protocol: "https"
      }
    ]
  },
  // The following parameters are directly passed to Typesense's search API endpoint.
  //  So you can pass any parameters supported by the search endpoint below. 
  //  queryBy is required. 
  additionalSearchParameters: {
    queryBy: "name"
  }
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const search = instantsearch({
  searchClient,
  indexName: "products"
});

// Helper for the render function
const renderIndexListItem = ({ indexId, hits }) => `
  <li>
    Index: ${indexId}
    <ol>
      ${hits
        .map(
          (hit, hitIndex) =>
            `
              <li>
                <p>${instantsearch.highlight({ attribute: 'name', hit })}</p>
                <button
                  type="button"
                  class="btn-add-to-cart"
                  data-index-id="${indexId}"
                  data-hit-index="${hitIndex}"
                >
                  Add to Cart
                </button>
              </li>
            `
        )
        .join('')}
    </ol>
  </li>
`;

// Create the render function
const renderAutocomplete = (renderOptions, isFirstRender) => {
  const { indices, currentRefinement, refine, widgetParams } = renderOptions;

  if (isFirstRender) {
    const input = document.createElement('input');
    const ul = document.createElement('ul');

    input.addEventListener('input', event => {
      refine(event.currentTarget.value);
    });

    widgetParams.container.appendChild(input);
    widgetParams.container.appendChild(ul);

    ul.addEventListener('click', (event) => {
      if (event.target.className === 'btn-add-to-cart') {
        const indexId = event.target.getAttribute('data-index-id');
        const hitIndex = event.target.getAttribute('data-hit-index');
        const index = indices.find(index => index.indexId === indexId);
        const hit = index.hits[hitIndex];

        index.sendEvent('conversion', hit, 'Product Added');
      }
    });
  }

  widgetParams.container.querySelector('input').value = currentRefinement;
  widgetParams.container.querySelector('ul').innerHTML = indices
    .map(renderIndexListItem)
    .join('');
};

// Create the custom widget
const customAutocomplete = connectAutocomplete(
  renderAutocomplete
);

// Instantiate the custom widget
search.addWidgets([
  index({ searchClient, indexName: 'landings' }),

  customAutocomplete({
    container: document.querySelector('#autocomplete'),
  })
]);

search.start();
</script>

Don’t forget to fill out the TYPESENSE_SEARCH_KEY and the TYPESENSE_HOST in your environment variables. And replace the first index products with your desired index. Replace ‘landings’ with your desired second index. You can add any more indices by adding:

index({ searchClient, indexName: 'anotherIndex' }),

to the search.AddWidgets part.

Mind that we also need to import the autocomplete connector and the index component widget via Laravel Mix.

Stappen ondernemen websites voor het AVG-proof maken

Met de komst van de AVG / GDPR wetgeving is er veel onduidelijkheid over best-practices of een duidelijk stappenplan voor het AVG-proof maken van je website. Wij zetten een aantal relevante bronnen uiteen. We hebben geen juridische achtergrond. Wij geven dan ook het disclaimer dat gebruik van deze informatie op eigen risico is.

Tools voor het maken van een privacy policy met AVG / GDPR-richtlijnen

Voor welke cookies toestemming vragen:

De Rijksoverheid is hier duidelijk in: https://www.rijksoverheid.nl/onderwerpen/telecommunicatie/vraag-en-antwoord/mag-een-website-ongevraagd-cookies-plaatsen

Indien je Google Anayltics gebruikt:

GA heeft specifieke instellingen nodig om als functionele cookie te voldoen en niet als tracking cookie:

https://autoriteitpersoonsgegevens.nl/sites/default/files/atoms/files/handleiding_privacyvriendelijk_instellen_google_analytics_mrt_2018.pdf

Indien je Hotjar gebruikt:

Conform webinar Hotjar, als je gegevens onderdrukt en keystrokes niet opslaat, hoef je geen toestemming volgens te vragen. Hotjar heeft ook wanneer je info invult een vinkje dat je kunt instellen.

Tekst die je in kunt voegen: https://docs.google.com/document/d/1Rp8kZg2JD2cVSiPK78UXBLCKSyJVb6HiPpME8k7iGIU/edit#

Tracking analytics en de GDPR wet

https://imu.nl/internet-marketing/avg-stappenplan-voor-internetondernemers/

https://help.hotjar.com/hc/en-us/articles/115012439167-Masking-Text-in-Recordings

Indien je een andere tracking gebruikt:

  • Mag je IP-adressen opslaan? Volgens mij niet zonder toestemming

Toestemming vragen voor de rest van je cookies

Plugins:

https://codecanyon.net/item/weepie-cookie-allow-easy-complete-cookie-consent-plugin/10342528

https://privacypolicies.com/cookie-consent/

of

Disabling cookies

Verdere info: http://www.iabeurope.eu/eucookielaws/nl/

https://www.acm.nl/sites/default/files/old_publication/publicaties/14496_veelgestelde-vragen-cookiebepaling-november-2016.pdf

Verder: https://www.rocketdigital.nl/blog/europese-cookiewet-2018/

Contactformulier: https://www.sowmedia.nl/wordpress/plugins/avg-gdpr-proof

Best practices: https://www.dailycms.com/artikelen/avggdpr-wat-betekent-deze-wetgeving-voor-websites-en-webwinkels/

https://www.rocketdigital.nl/blog/avg-checklist/

 

 

 

3 of the best Javascript Searchable Select Boxes compared – Select2 vs Chosen vs Selectize.js performance

When having large amounts of jQuery/javascript select boxes that need to be searchable it is wise looking at the performance of them. I compared the following select replacement boxes: Select2, Chosen, Selectize.js, Bselect. Default Safari browser select box: initializing about 0.5s.

Setup

Having a backend page that has about 1000 select boxes that need to be initialized by a searchable/autocomplete selectbox. I checked the loading times of the page and the time of the initializitation of the select box. I measured it first by hand and later on by Safari web console.

Chosen performance

  • Measuring by hand: about 8s-11s.
  • Console measure: about 12s.Chosen select box performance

Cons:

  • Has no ajax support, if you want to do this, you can combine it with select2.

Select 2 performance

  • Measuring by hand: initializing takes about 9s-16s.
  • Console meaure:  about 21.69s.Select2 select box performance

Selectize.js performance

Selectize.js: about 19s. Console crashes after 29s. So, I exclude further results.

Bselect performance

Bselect: more than 40s, I got bored counting, so I exclude further results.

And the winner is … Chosen

It is initializes as fastest, being about 1,5x as fast as Select2. Although, Select2 has lot more functionalities and is better maintained. When you only need simple searchable select boxes and mind about performance, Chosen is the best choice. Note that it is only one simple configuration, it might be that in other configurations Select2 is faster than Chosen.

jQuery Address and History Forms – Remembering form choices in hash / browserstate (updated version)

For jQuery hash support I like the jQuery Address plugin: http://www.asual.com/jquery/address/ . Especially, if you want to make a form or product filter, you can use this variant: http://www.asual.com/jquery/address/samples/form/ .

Though, it doesn’t support checkboxes in the form of arrays like check[]=1&check[]=2 , etc. I updated the plugin with some other libraries and code and now it works.

You can download it here.