When you type three words into Google and get the answer in a fraction of a second, with your typos corrected, it's not a SQL database parsing billions of pages: it's an inverted index, a dictionary mapping "word → pages containing it". Elasticsearch and OpenSearch apply exactly this principle to your own data. And when Netflix suggests "people who liked this also watched…", or Facebook suggests a friend-of-a-friend, those are relationships being traversed, not rows being filtered: that's the job of a graph database like Neo4j.
| Who | Tool | Why |
|---|---|---|
| Wikipedia | Elasticsearch | Search bar for all articles, in all languages, typo-tolerant |
| Netflix, Uber, Slack | Elasticsearch + Kibana | Billions of log lines per day, explored live to find an outage in seconds |
| Stack Overflow, GitHub | Elasticsearch | Full-text search for questions, answers, and code |
| Amazon | OpenSearch | The engine Amazon created and maintains to keep a 100% open-source version, sold as a managed service on AWS |
| NASA | Neo4j | Database of "lessons learned" from 50 years of missions, linked together to find a precedent in a few hops |
| ICIJ Consortium (Panama Papers) | Neo4j | 11.5 million documents and links between shell companies, executives, and banks, explored as a graph by 370 journalists |
| eBay, Walmart | Neo4j | Real-time recommendations: "people who bought this…", calculated by following purchase relationships |
What these companies have in common: they also have SQL databases for payments, orders, invoices. They add a search engine alongside for searching, and a graph database alongside for linking. That's exactly what you're going to build in this lab, in miniature.
Imagine a large online school. At the front desk, a librarian finds in one second all courses talking about "deploying containers", even if you wrote "deployment" without an accent: that's Elasticsearch. On the walls, screens display the site's live traffic, error pages, visitor countries: that's Kibana, which stores nothing but draws what Elasticsearch contains. In the academic advising office, a counselor knows the links between people: who took what, which course is a prerequisite for which, which students are similar: that's Neo4j. And OpenSearch? It's the twin librarian, trained at the same school as Elasticsearch, hired by those who want a 100% open-source contract. Four tools, two ways to organize information: by words (search engine) or by links (graph).
| Tool | Lab Version | Role | You Talk to It With |
|---|---|---|---|
| Elasticsearch | 9.5.3 | Stores JSON documents and retrieves them by text, filters, aggregations | Query DSL (JSON), ES|QL |
| Kibana | 9.5.3 | Web interface for Elasticsearch: Dev Tools, Discover, Lens, dashboards | Clicks, KQL, and Dev Tools for JSON |
| OpenSearch (+ Dashboards) | 3.8.0 | Open-source fork of Elasticsearch and Kibana, 95% same API | Same Query DSL, plus SQL and PPL |
| Neo4j Community | 5.26 | Graph database: nodes, relationships, properties | Cypher |
Elasticsearch and OpenSearch share the same internal engine, Apache Lucene, and the same principle: the inverted index. Instead of reading every document on each search, the engine builds once and for all a dictionary "word → list of documents containing it". Searching "docker kubernetes" then amounts to intersecting two lists of IDs, which takes milliseconds on millions of documents. Neo4j makes the opposite bet: it stores relationships as physical pointers between nodes, so "friends of my friends" is calculated by following arrows, without joins.
Each engine invents its own terminology, but they often refer to the same idea as in relational databases. This table translates basic terms between classic SQL, search engines, and graphs.
| Classic SQL | Elasticsearch / OpenSearch | Neo4j (Graph) | In Plain English |
|---|---|---|---|
| Database | Cluster (collection of indexes) | Graph database | All content managed by the server |
| Table | Index | Node label | A collection of similar elements |
| Row / Record | Document (JSON) | Node | One element |
| Column | Field | Property | An attribute of the element |
| Schema / DDL | Mapping | Flexible schema (optional) | Definition of field types |
| Primary key | _id of the document | Node identity | Unique identifier |
| Foreign key + join | No join: cours_id denormalized | Relationship (-[:PREREQUIS_OF]->) | Link between two elements |
| Index (B-tree) for acceleration | Inverted index | Index on a property | Structure that speeds up search |
SELECT … WHERE | Query DSL (JSON), ES|QL | MATCH … WHERE … RETURN | Query the data |
GROUP BY / Aggregate | Aggregations (aggs) | count(), collect() | Group and count |
| SQL (language) | Query DSL / ES|QL / KQL | Cypher | Query language |
Beware the word "index". In SQL, an index is an acceleration structure (B-tree) placed on a table. In Elasticsearch, an index is the equivalent of the table itself. And the inverted index is yet another thing: the internal mechanism "word → documents" that makes search instant. Three meanings for one word.
Let's take three concepts from our school: courses, students, reviews left by students on courses.
1. In SQL (tables and joins) — three normalized tables, information is never duplicated, links go through foreign keys:
SELECT c.titre, AVG(a.note)
FROM cours c
JOIN avis a ON a.cours_id = c.id
JOIN etudiant e ON e.id = a.etudiant_id
WHERE e.ville = 'Montréal'
GROUP BY c.titre;Perfect for consistency (a changed review is changed everywhere), less good for "all courses whose description contains deploy": LIKE '%deploy%' scans the entire table.
2. In Documents (Elasticsearch / OpenSearch) — a course is a single JSON document that includes what search needs, including the teacher's name and average rating already calculated. Here, exactly as is, the first document from the cours index in the lab:
{
"id": "C0001",
"titre": "Docker expliqué simplement",
"description": "Dans ce cours accessible sans prérequis, vous apprenez à automatiser vos applications avec Docker. …",
"categorie": "DevOps", "sujet": "Docker", "niveau": "debutant", "langue": "en",
"prix": 129, "gratuit": false, "duree_heures": 5,
"tags": ["docker", "linux", "helm", "devops"],
"date_publication": "2024-08-14",
"note_moyenne": 4.4, "nb_avis": 327,
"professeur": { "id": "P001", "nom": "Karim Caron", "ville": "Gatineau" },
"competences": ["Conteneurisation", "Intégration continue"]
}The teacher's name is denormalized (copied into each course). If he changes his name, you'd need to reindex his courses: that's the price of instant search. Reviews live in a second index, avis, with cours_id as a simple field, no join possible at query time.
3. In Graph (Neo4j) — the same concepts become nodes, and links become first-class relationships that carry their own properties. Here are three real relationships from the lab graph around course C0001:
(:Etudiant {prenom: "Hugo"})-[:INSCRIT_A {progression: 100, note: 4}]->(:Cours {id: "C0001", titre: "Docker expliqué simplement"})
(:Cours {id: "C0001"})-[:PREREQUIS_DE]->(:Cours {id: "C0003", titre: "Docker : le guide complet"})
(:Professeur {prenom: "Karim", nom: "Caron"})-[:ENSEIGNE]->(:Cours {id: "C0001"})The question "which courses have been taken by students who liked the same course as me?" is written in one line of Cypher and calculated by following three relationships, where SQL would chain three joins and Elasticsearch simply wouldn't know how to answer.
| Question | Best Tool | Why |
|---|---|---|
| "courses about deploying containers, typo-tolerant" | Elasticsearch / OpenSearch | Inverted index, French analyzer, fuzzy |
| "Distribution of 500 errors by country over 30 days" | Elasticsearch + Kibana | Aggregations and real-time visualization |
| "Path of prerequisites to reach course C0230" | Neo4j | Relationship traversal, shortest path |
| "Recommend a course based on similar enrollments" | Neo4j | Collaborative filtering = graph pattern |
| "Payment, invoice, strict consistency" | SQL (outside lab) | ACID transactions |
In real life, logs arrive continuously (Filebeat, Logstash, or the app itself) and the graph database is fed by the business system. In the lab, two commands replace all that: importer sends NDJSON files to Elasticsearch's _bulk API, charger-graphe runs a Cypher script that reads CSVs. The two worlds don't communicate: you, or your app, choose who to ask each question.
Nothing to type in this lesson: the lab starts in lesson 03. The excerpts below were taken from the lab itself, so you know what you'll find in it.
| Set | Exact Volume | Content |
|---|---|---|
courses index | 504 documents | Title, description (French analyzer), category, subject, level, price, tags, average rating, teacher |
reviews index | 609 documents | Rating 1-5, text, first name, city, country, date, "helpful" votes |
access index | 12,000 lines | Web logs from 2026-08-10 to 2026-09-08: path, HTTP status, IP, country, device, referrer |
| Neo4j graph | 872 nodes · 3,712 relationships | Courses 504, Students 300, Teachers 30, Skills 22, Cities 16 |
What to notice: the 504 courses in the graph are the same as those in the courses index (same IDs C0001…C0504). You can search for a course in Elasticsearch then explore its prerequisites in Neo4j.
terms aggregation on categorie returns:Cloud 84 · DevOps 84 · Data 84 · Web Development 84 · AI 84 · Security 84And on other fields: level → beginner 252, advanced 158, intermediate 94 ; language → fr 375, en 129 ; free → 75 free courses ; 72 distinct subjects (Docker, Kubernetes, Elasticsearch, Neo4j, React, AWS…) ; 30 teachers. What to notice: round numbers known in advance, convenient for checking your queries in modules 2 and 3.
reviews index:{ "id": "A00001", "cours_id": "C0028", "etudiant": "Nathan", "ville": "Sherbrooke", "pays": "Canada",
"note": 3, "texte": "Les vidéos sont bonnes, la partie théorique est dense.", "date": "2024-06-21", "utile": 33 }What to notice: cours_id is a plain string. No join links this review to its course in Elasticsearch; your app makes the connection.
access index:{ "id": "L012000", "@timestamp": "2026-09-08T23:39:29.000Z", "methode": "GET", "chemin": "/cours/C0430",
"cours_id": "C0430", "categorie": "Cloud", "statut": 200, "octets": 168403, "duree_ms": 351,
"ip": "169.199.97.7", "pays": "CA", "appareil": "desktop", "navigateur": "Chrome", "referent": "direct" }Of the 12,000 lines: 10,829 in 200, 403 in 404, 355 in 301, 235 in 304, 122 in 500, 56 in 503. What to notice: the @timestamp field, essential to Kibana Discover for the time axis (module 4).
Courses 504 · Students 300 · Teachers 30 · Skills 22 · Cities 16
ENROLLS_IN 1654 · COVERS 994 · TEACHES 504 · LIVES_IN 330 · PREREQUISITE_FOR 230What to notice: 1,654 enrollments for 300 students, or about 5.5 courses per student. It's this mesh that will make the recommendations in module 7 interesting.
"I know SQL, why not do everything with PostgreSQL?" → You can, up to a point. PostgreSQL can do full-text search and recursive queries. But as soon as you want typo tolerance, relevance ranking, aggregations on millions of rows in real time, or variable-depth graph traversal, each specialized engine does the job in milliseconds where SQL takes exotic indexes and unreadable queries. The right reflex: SQL for transactional truth, Elasticsearch alongside for searching, Neo4j alongside for linking.
"Elasticsearch or OpenSearch, do I have to choose now?" → No. This course has you work with Elasticsearch; module 5 replays the same queries on OpenSearch and lists the differences (a handful). What you learn applies to both.
"Kibana, is it a database?" → No, and it's a common confusion. Kibana only stores your configuration (dashboards, views) in a hidden Elasticsearch index. All data you see comes from Elasticsearch; if Elasticsearch is down, Kibana displays "Kibana server is not ready yet" (lesson 04).
The word "index" means two different things: in SQL, a B-tree acceleration structure attached to a table; in Elasticsearch, the equivalent of the table itself, split into shards (autonomous Lucene indexes). Module 2, lesson 01, covers this vocabulary in practice on the lab.