projects→first-aid-group-finder-semantic-search

First-Aid Group Finder / Matching Unlinked Code Systems with Semantic Search

Thousands of codes, no official link between them: semantic search does the first pass, the human in the loop has the last word.

How semantic search with OpenAI embeddings matched two unlinked code systems: 3,257 Italian business codes and their first-aid risk groups, with a human in the loop.

Vintage green first-aid tin full of index cards with colored tabs, on a saffron background
Status: live Type: Data & ML Stack:
  • Chroma
  • Embeddings
  • JavaScript
  • Jupyter
  • LangChain
  • OpenAI API
  • pandas
  • PHP
  • Python
  • WordPress

The problem

In Italy, every company is required by law1 to organize its first aid according to its group: A, B or C. The group decides how long the mandatory course is, how often it must be refreshed, and what first-aid kit is required. And it depends on two things: the number of workers, and the company’s risk level, measured by INAIL, the national workplace insurance institute.

The catch is that companies know their ATECO code (the official classification of economic activities), not their INAIL risk group. Both tables are long: thousands of ATECO codes on one side, and on the other, INAIL’s groups, broken down into hundreds of tariff items, each with its own description. And there’s no official correspondence between the two. Matching them means reading descriptions and interpreting them, and that has always required a professional. It still does, but this is where the tool described on this page comes in: to make that work faster and more efficient.

My client, an occupational physician, asked me to develop it for his website: a company enters its ATECO code (or its INAIL group) and the number of workers, and gets its likely first-aid group in seconds. The tool does the first pass; the professional takes it from there.

The tool

On the website, the visitor picks either an INAIL group or an ATECO code, then the number of workers. One click, and the result appears with a traffic light:

GroupMandatory courseRefresherFirst-aid equipment
🔴 A16 hoursevery 3 years, 6 hoursfirst-aid box
🟡 B12 hoursevery 3 years, 4 hoursfirst-aid box
🟢 C12 hoursevery 3 years, 4 hoursbasic medical kit

When the ATECO code could belong to groups with different outcomes, the tool doesn’t guess. It says the case is ambiguous, and invites the company to ask an expert.

From ATECO to INAIL: semantic search

The INAIL route is simple: each INAIL group has an official risk index, and above 4 the company is in Group A. The hard part is the ATECO route: deciding which INAIL group each of the 3,257 ATECO codes belongs to. After discussing it with my client, I computed that mapping in advance, once2, and stored it in a table in the WordPress database, which the tool looks up.

To build it, I used semantic search. Each description is turned into an embedding, a list of numbers that captures its meaning, so texts with similar meanings end up close together even when they use different words. Then, for each ATECO code, the system looks for the closest INAIL groups:

Every code also gets a one-line explanation of how its verdict was reached: that’s for the human in the loop.

A human in the loop

Pure semantic similarity makes odd suggestions. Among its candidates, the search proposed agricultural machinery for aircraft manufacturing, and tourism and catering for data processing. So every run was followed by a review.

I went through the codes the pipeline had flagged as uncertain one by one3, around ninety of them, and decided where each belonged. For a few tricky ones, I asked ChatGPT for a second opinion. Then, instead of patching the final table, I fed each decision back into the search index as a short keyword “anchor”: for example, “battery manufacturing” pointing to the chemicals group.

That choice mattered. A patched table forgets its fixes the next time it’s rebuilt. An anchor in the index stays there: every new run reproduces the fix, and it also pulls similar codes in the right direction. Over several runs, the index grew from 117 descriptions to 165, and each new version was compared with the previous one before going online.

Results and limits

In the final run, all 3,257 ATECO codes got a verdict:

Almost every code was settled by full agreement among its candidates. Only a handful needed the weighted vote.

The limits are worth stating plainly:

  1. Ministerial Decree 388/2003 (Decreto Ministeriale 15 luglio 2003, n. 388). I’m neither a lawyer nor a doctor, so please check the details with a professional. ↩︎
  2. Well, it took a few attempts and plenty of tests to get it as right as possible. That’s the part where a human has to check the AI’s work. ↩︎
  3. I set up the pipeline to flag them automatically: whenever the candidate groups disagreed and none got more than 60% of the vote, the code was marked uncertain. And each new run was compared with the previous one, so any code whose verdict changed stood out for review. ↩︎

contact

Let's talk.

Need help with something? Looking for a professional who's reliable and believes in karma*?

Tell me about it here, or send me an email. I reply within two working days.

* which means value and integrity come first; reward follows.