Symfony meets Elasticsearch — Implement a search as you type feature

What do we build?

What is Elasticsearch?

Development setup

docker run --rm -it -v $PWD:/app composer create-project symfony/website-skeleton symfony_elasticsearch
version: '3'
services:
php:
image: webdevops/php-nginx-dev:7.4
working_dir: /app
environment:
- WEB_DOCUMENT_ROOT=/app/public
- PHP_DISPLAY_ERRORS=1
- PHP_MEMORY_LIMIT=2048M
- PHP_MAX_EXECUTION_TIME=-1
- XDEBUG_REMOTE_AUTOSTART=1
- XDEBUG_REMOTE_PORT=9000
- XDEBUG_PROFILER_ENABLE=0
- XDEBUG_REMOTE_CONNECT_BACK=0
- XDEBUG_REMOTE_HOST=docker.for.mac.localhost
- php.xdebug.idekey=PHPSTORM
- php.xdebug.remote_enable=1
- php.xdebug.max_nesting_level=1000
ports:
- "8080:80"
volumes:
- ./:/app:rw,cached
es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.9.0
environment:
- "discovery.type=single-node"
- "bootstrap.memory_lock=true"
- "ES_JAVA_OPTS=-Xms1G -Xmx1G"
- "xpack.security.enabled=false"
- "http.cors.enabled=true"
- "http.cors.allow-origin=*"
ports:
- 9201:9200

Discover Elasticsearch via the Head plugin

Google Chrome Head plugin in action

Symfony integration

docker run --rm -it -v $PWD:/app composer require elasticsearch/elasticsearch
parameters:
es_config: {'hosts': ['http://es:9200']}

services:
# lots of service definitions in between
Elasticsearch\ClientBuilder: ~

Elasticsearch\Client:
factory: ['@Elasticsearch\ClientBuilder', fromConfig]
arguments: ['%es_config%']

Feed Elasticsearch

Execute the feeder command

docker-compose exec php bin/console app:feed_products
Index with product documents

Check the analyzer

Custom autocomplete analyzer output for “Samsung Galaxy S20”
Analyzed text of the search input “Samsung Galaxy S20”
Debug output of the custom autocomplete analyzer
Debug a filter for analyzers

Analyze the index feeding code

Autocomplete UI with Webpack Encore

docker run --rm -it -v $PWD:/app composer require symfony/webpack-encore-bundledocker run --rm -it -v $PWD:/app -w /app node:alpine yarn install
docker run --rm -it -v $PWD:/app -w /app node:alpine yarn add bootstrap sass-loader autocomplete.js node-sass --dev
docker-compose exec php php bin/console make:controller AutoCompleteController
Project structure including a Controller and a Template
{% extends 'base.html.twig' %}

{% block title %}Hello AutoCompleteController!{% endblock %}

{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}

{% block body %}
<div class="container">
<div class="row justify-content-md-center">
<div class="col-sm-6 col-sm-offset-3 mt-5">
<form action="#" class="form">
<h2>Elasticsearch search as you type</h2>
<input class="form-control" id="search-input" name="contacts" type="text" placeholder='Search by name' />
</form>
</div>
</div>
</div>
{% endblock %}

Code the frontend

Assets structure in the project
// customize some Bootstrap variables
$primary: darken(#428bca, 20%);

// the ~ allows you to reference things in node_modules
@import "~bootstrap/scss/bootstrap";

.algolia-autocomplete {
width: 100%;
}
.algolia-autocomplete .aa-input, .algolia-autocomplete .aa-hint {
width: 100%;
}
.algolia-autocomplete .aa-hint {
color: #999;
}
.algolia-autocomplete .aa-dropdown-menu {
width: 100%;
background-color: #fff;
border: 1px solid #999;
border-top: none;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion {
cursor: pointer;
padding: 5px 4px;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion.aa-cursor {
background-color: #B2D7FF;
}
.algolia-autocomplete .aa-dropdown-menu .aa-suggestion em {
font-weight: bold;
font-style: normal;
}
Basic Bootstrap colors
// any CSS you import will output into a single css file (app.css in this case)
import '../css/app.scss';

import autocomplete from 'autocomplete.js';

autocomplete('#search-input', { hint: false }, [{
source: function(query, cb) {
fetch("/ac/search?q="+query)
.then(response => response.json())
.then(data => cb(data.products));
}
}]);
docker run --rm -it -v $PWD:/app -w /app node:alpine yarn run build
public folder of the Symfony project
Generated manifest.json file
<?php

namespace App\Controller;

use Elasticsearch\Client;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class AutoCompleteController extends AbstractController
{
/**
*
@Route("/ac", name="auto_complete")
*/
public function index()
{
return $this->render('auto_complete/index.html.twig', [
'controller_name' => 'AutoCompleteController',
]);
}

/**
*
@Route("/ac/search", name="auto_complete_search", methods="GET")
*/
public function search(Client $client, array $indexDefinition, Request $request)
{
$query = $request->query->get('q');

$result = $client->search(
array_merge(
$indexDefinition,
['body' => [
'query' => [
'match' => [
'title' => [
'query' => $query,
"operator" => "and",
"fuzziness" => 2,
"analyzer" => "standard"
]
],
],
'size' => 3
]]
));

$data = array_map(function ($item) {
return ['value' => $item['_source']['title']];
}, $result['hits']['hits']);

return $this->json([
'products' => $data
]);
}
}

Browse the UI

Search as you type example

Summary

Software Architect

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store