ūüźô un moteur de recherche d'√©mojis

- 13 minutes de lecture - 2628 mots

Pour l’un de mes projets, j’ai d√Ľ g√©rer des emojis. Le but √©tait de cr√©er un moteur de recherche d’emojis. Je ne pars pas de rien, car je dois inclure le tout dans l’un de mes programmes qui tourne d√©j√†, et c’est en Go. Regardons ensemble comment construire un petit moteur de recherche en Go.

Pour les plus impatients, l’ensemble des exemples de code de cet article se trouve ici : git2.riper.fr/ztec/emoji-search-engine-go

Vous pouvez aussi tester et voir le r√©sulta final. Tous les d√©tails son ici: poulpe.ztec.fr - Le moteur de recherche d’emoji open-sourc√©

🐗 √Čmojis! Attrapez-les tous!

En premier lieu, il m’a fallu trouver la liste de tous les emojis qui existent. Le site de l’Unicode en met une √† disposition :

https://unicode.org/Public/emoji/15.0/emoji-test.txt

Le fichier ressemble à ceci :

[…]
# group: Smileys & Emotion

# subgroup: face-smiling
1F600                                                  ; fully-qualified     # ūüėÄ E1.0 grinning face
1F603                                                  ; fully-qualified     # ūüėÉ E0.6 grinning face with big eyes
1F604                                                  ; fully-qualified     # ūüėĄ E0.6 grinning face with smiling eyes
1F601                                                  ; fully-qualified     # ūüėĀ E0.6 beaming face with smiling eyes
1F606                                                  ; fully-qualified     # ūüėÜ E0.6 grinning squinting face
1F605                                                  ; fully-qualified     # ūüėÖ E0.6 grinning face with sweat
[…]

On y trouve le code Unicode, l’√©moji lui-m√™me et une description. Le fichier est pr√©vu pour les machines, il devrait donc √™tre facile √† parser.

Avant de se lancer t√™te baiss√©e dans cette direction, regardons ce que la communaut√© a d√©j√† fait sur le sujet. J’ai trouv√© :

En regardant dans le code de ces projets j’ai trouver un truc tr√®s int√©ressant : https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json

Le fichier, qui est mis √† jour r√©guli√®rement, est parfait et peut √™tre encore plus facilement pars√©. C’est du JSON. En plus de √ßa, il contient quelques informations suppl√©mentaires comme les alias.

This is the way, on va parser ce fichier directement !

package pouet

import (
	"encoding/json"
	"github.com/go-zoox/fetch"
)

type EmojiDescription struct {
	Emoji          string   `json:"emoji"`
	Description    string   `json:"description"`
	Category       string   `json:"category"`
	Aliases        []string `json:"aliases"`
	Tags           []string `json:"tags"`
	HasSkinTones   bool     `json:"skin_tones,omitempty"`
	UnicodeVersion string   `json:"unicode_version"`
}

type GithubDescriptionResponse []EmojiDescription

func fetchEmojiFromGithub() (results []EmojiDescription, err error) {
	response, err := fetch.Get("https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json")
	if err != nil {
		return
	}
	err = json.Unmarshal(response.Body, &results)
	return
}

J’ai utilis√© github.com/go-zoox/fetch pour r√©cup√©rer le fichier, car je suis paresseux.

🦓 √Čmoji, Scannez-les tous!

Dans mon programme, j’utilise d√©j√† Bleve pour indexer d’autres trucs. Je vais donc l’utiliser ici aussi. L’op√©ration est plut√īt simple, car je n’ai pas √† conserver de copie de l’index, juste une version en m√©moire suffit.

package pouet

import (
	"fmt"
	"github.com/blevesearch/bleve/v2"
	"github.com/sirupsen/logrus"
	"strconv"
	"strings"
)

var (
	index  bleve.Index
	emojis []EmojiDescription
)

func indexEmojis() error {
	// we create a new indexMapping. I used the default one that will index all fields of my EmojiDescription
	mapping := bleve.NewIndexMapping()
	// we create the index instance
	bleveIndex, err := bleve.NewMemOnly(mapping)
	if err != nil {
		return err
	}
	// we fetch the emoji from the internet. This can fail, and may be embedded for better performance
	e, err := fetchEmojiFromGithub()
	if err != nil {
		logrus.WithError(err).Error("Could fetch emoji list")
		return err
	}
	emojis = e
	for eNumber, eDescription := range emojis {
		// this will index each item one by one. No need to be quick here for me, I can wait few ms for the program to start
		err = bleveIndex.Index(fmt.Sprintf("%d", eNumber), eDescription)
		if err != nil {
			logrus.WithError(err).Error("Could not index an emoji")
		}
	}
	index = bleveIndex // we make the index available
}

Une fois que la fonction indexEmojis est appel√©e, j’ai un index pr√™t √† l’emploi pour chercher des √©mojis. Testons-le.

package pouet

import (
	"fmt"
	"github.com/AkinAD/emoji"
	"github.com/blevesearch/bleve/v2"
	"github.com/sirupsen/logrus"
	"strconv"
	"strings"
)

var (
	index  bleve.Index
	emojis []EmojiDescription
)

func Search(q string) (results []EmojiDescription) {
	if index == nil {
		// no Index mean indexEmojies was not called yet or did not finished. No results (boot process)
		return
	}
	// we create a query as bleve expect.
	query := bleve.NewQueryStringQuery(q)
	// we define the search options and limit to 200 results. This should be enough.
	searchrequest := bleve.NewSearchRequestOptions(query, 200, 0, false)
	// we do the search itself. This is the longest. Approximately few hundreds of us 
	searchresults, err := index.Search(searchrequest)
	if err != nil {
		logrus.WithError(err).Error("Could not search for an emoji")
		return
	}
	
	// we return the results. I use the index to find my original object stored in `emojis` because it's simpler. Optimisation possible.
	for _, result := range searchresults.Hits {
		numIndex, _ := strconv.ParseInt(result.ID, 10, 64)
		results = append(results, emojis[numIndex])
	}
	return
}

J’ai choisi d’utiliser NewQueryStringQuery car il permet pas mal d’options lors de la recherche, directement via la cha√ģne. Comme √ßa je pourrais ajouter des modificateurs pour affiner mes recherches. J’utilise beaucoup ces options pour les autres trucs que j’indexe, √ßa ne sera peut-√™tre pas si utile que √ßa sur des √©mojis, mais √ßa ne co√Ľte pas grand-chose alors je le garde quand m√™me.

Détendez-vous et imaginez un clip musical de moi qui ajoute le code que vous avez vu dans mon programme et créant une superbe interface pour envoyer les recherches et voir les résultats.

🧋 Recherche approximative (Fuzzy)

C’est cool, les r√©sultats sont bons, mais il semblerait qu’il y ait des rat√©s.

Résultat de la recherche `hug` qui n'affiche pas de résultats
R√©sultat de la recherche hug qui n’affiche pas de r√©sultats

Ici, je devrais avoir un √©moji en r√©sultat, c’est 🤗!. Si j’ajoute le s √† la requ√™te, le moteur le trouve, mais pas sans. Essayons d’am√©liorer √ßa en acceptant des r√©sultats approximatifs.

L’id√©e est de chercher les r√©sultats proches de la recherche souhait√©e, m√™me s’ils ont un ou deux caract√®res de diff√©rent. Pour faire √ßa, on va utiliser un truc qui s’appelle la Distance de Levenshtein. C’est cool, car Bleve int√®gre d√©j√† ce m√©canisme. Malheureusement, je n’ai pas trouv√© comment l’utiliser avec les QueryStringQuery, notamment pour ajouter un niveau d’approximation par d√©faut. Je peux toujours ajouter un ~ apr√®s un mot pour l’activer sur celui-ci, mais ce n’est pas pratique.

C’est un petit projet perso, alors on va y aller en suivant la m√©thode RACHE. Si je n’ai pas de r√©sultats avec la premi√®re m√©thode, je tente avec une recherche d√©di√©e.

func Search(q string) (results []EmojiDescription) {
	if index == nil {
		// no Index mean indexEmojies was not called yet or did not finished. No results (boot process)
		return
	}
	// we create a query as bleve expect.
	query := bleve.NewQueryStringQuery(q)
	// we define the search options and limit to 200 results. This should be enough.
	searchrequest := bleve.NewSearchRequestOptions(query, 200, 0, false)
	// we do the search itself. This is the longest. Approximately few hundreds of us 
	searchresults, err := index.Search(searchrequest)
	if err != nil {
		logrus.WithError(err).Error("Could not search for an emoji")
		return
	}
	
	// If we have no results we try to do a basic fuzzy search
	if len(searchresults.Hits) == 0 {
		// this time, we create a fuzzy query. The rest is the same as before. CopyPasta style. 
		fuzzyQuery := bleve.NewFuzzyQuery(q)
		searchrequest := bleve.NewSearchRequestOptions(fuzzyQuery, 200, 0, false)
		searchresults, err = index.Search(searchrequest)
		if err != nil {
			logrus.WithError(err).Error("Could not search for emoji")
			return
		}
	}
	// we return the results. I use the index to find my original object stored in `emojis` because it's simpler. Optimisation possible.
	for _, result := range searchresults.Hits {
		numIndex, _ := strconv.ParseInt(result.ID, 10, 64)
		results = append(results, emojis[numIndex])
	}
	return
}
Résultat de la recherche `hug` qui affiche maintenant plusieurs résultats dont l'emoji `hug`
R√©sultat de la recherche hug qui affiche maintenant plusieurs r√©sultats dont l’emoji hug

Cette fois, c’est bon, j’ai bien mon √©moji c√Ęlin. J’ai √©galement quelques autres r√©sultats, mais √ßa va. Je ne m’attends pas √† avoir mon r√©sultat en premier, du moment qu’il est visible sans descendre dans la page, √ßa me convient.

note: j’aurais pu aussi utiliser une recherche par pr√©fixe, mais je ne cherche pas toujours en utilisant le d√©but du nom des √©mojis, donc je pr√©f√®re la recherche fuzzy

🟪 Les couleurs de peau

Si je cherche pour l’√©moji ok hand, je le trouve. Cependant, il n’y a que la version de base, la jaune. J’aimerais bien aussi voir les variations quand il y en a.

Résultat de la recherche `ok hand` n'affichant que des émoji jaune
R√©sultat de la recherche ok hand n’affichant que des √©moji jaune

D√©tendez-vous une seconde encore, et imaginez qu’un narrateur fait irruption dans votre t√™te avec une voix profonde et raconte : “Zed ne le sait pas encore, mais inclure ces jolis √©mojis avec toutes les couleurs de peau sera une t√Ęche difficile. Des heures passeront avant qu’il ne r√©ussisse le challenge et qu’il comprenne enfin”.

Avant de continuer, quelques explications sur la fa√ßon dont les √©mojis fonctionnent. Ce sont des caract√®res UTF-8. Ces caract√®res peuvent √™tre combin√©s ensemble pour former ce qu’on appelle des ligatures. Vous prenez deux codes UTF-8 caract√®res et vous les collez ensemble en un seul caract√®re. Sur votre √©cran, vous verrez alors un autre caract√®re qui n’est aucun des deux premiers. Dans les textes, c’est utilis√© pour les liaisons graphiques et pour rendre le texte lisible quand deux lettres simplement coller l’une √† c√īt√© de l’autre le sont moins. La beaut√© du concept, c’est que si votre police ou votre √©cran ne supporte pas ces ligatures, vous verrez toujours les deux premiers caract√®res. Cool, non ?

La couleur de peau d’un √©moji est g√©r√©e avec des ligatures. Vous prenez un √©moji, et vous y collez le caract√®re de la couleur de peau que vous voulez. Le r√©sultat sera un nouvel √©moji avec le jaune remplac√© par la couleur choisie. Bien s√Ľr, il faut que la police de caract√®res le supporte, donc toutes les combinaisons ne sont pas possibles.

1F44C                                                  ; fully-qualified     # ūüĎĆ E0.6 OK hand
1F44C 1F3FB                                            ; fully-qualified     # ūüĎĆūüŹĽ E1.0 OK hand: light skin tone
1F44C 1F3FC                                            ; fully-qualified     # ūüĎĆūüŹľ E1.0 OK hand: medium-light skin tone
1F44C 1F3FD                                            ; fully-qualified     # ūüĎĆūüŹĹ E1.0 OK hand: medium skin tone
1F44C 1F3FE                                            ; fully-qualified     # ūüĎĆūüŹĺ E1.0 OK hand: medium-dark skin tone
1F44C 1F3FF                                            ; fully-qualified     # ūüĎĆūüŹŅ E1.0 OK hand: dark skin tone

La premi√®re colonne contient le code UTF-8 de chaque √©moji. On voit bien que la premi√®re partie ne change pas. C’est le code de 👌. Le second code est la couleur de peau. Nous avons donc la liste des couleurs de peau disponibles.

	tones := map[string][]rune{
      "light skin tone" : []rune("\U0001F3FB"),
      "medium-light skin tone" : []rune("\U0001F3FC"),
      "medium skin tone" : []rune("\U0001F3FD"),
      "medium-dark skin tone" : []rune("\U0001F3FE"),
      "dark skin tone" : []rune("\U0001F3FF"),
	}

Dans les librairies dont j’ai parl√© en d√©but d’article, les √©mojis et leurs codes sont g√©r√©s via des string et utilisent la syntaxe sp√©cifique de l’UTF-8 en Go (\Uxxxxxxxx). Golang poss√®de cependant un type d√©di√© √† la manipulation de caract√®res UTF-8, la rune. J’ai d√©cid√© de l’utiliser. Malheureusement, il y a vraiment peu d’exemples en ligne qui utilisent les runes, surtout avec des ligatures. J’ai utilis√© la repr√©sentation en string ici pour que l’on voie bien le code et le lien entre les runes et le caract√®re.

Maintenant, on a besoin de cr√©er un nouvel √©moji pour chaque variation de couleur. Tous les √©mojis ne supportent pas ces variations. Je pourrais parser le fichier original d’Unicode, mais je suis paresseux, vous savez. En plus, si vous avez fait attention avant, le fichier qu’on parse d√©j√† poss√®de un champ qui donne cette information sous forme d’un bool, il n’y a donc rien √† faire. 🎉

func enhanceEmojiListWithVariations(list []EmojiDescription) []EmojiDescription {
	tones := map[string][]rune{
        "light skin tone" : []rune("\U0001F3FB"),
        "medium-light skin tone" : []rune("\U0001F3FC"),
        "medium skin tone" : []rune("\U0001F3FD"),
        "medium-dark skin tone" : []rune("\U0001F3FE"),
        "dark skin tone" : []rune("\U0001F3FF"),
    }
	for _, originalEmoji := range list {
		// we only add variations for emoji that supports it
		if originalEmoji.HasSkinTones {
			// we do it for every skin tone
			for skinToneName, tone := range tones {
				// we make a copy of the emojiDescription
				currentEmojiWithSkinTone := originalEmoji
				
				// This is the important bit that took me hours to figure out
				// we convert the emoji in rune (string -> []rune). An emoji can already be composed of multiple sub UTF8 characters, therefore multiple runes.
				// we append to the list of runes the one for the skin tone.
				// finally, we convert that in string using the type conversion. Using fmt would result in printing all runes independently
				currentEmojiWithSkinTone.Emoji = string(append([]rune(currentEmojiWithSkinTone.Emoji), tone...))
				
				// we adapt the description and metadata to match the skin tone
				currentEmojiWithSkinTone.Description = fmt.Sprintf("%s %s", currentEmojiWithSkinTone.Description, skinToneName)
				aliases := []string{}
				for _, alias := range currentEmojiWithSkinTone.Aliases {
					// we update all aliases to include the skin tone
					aliases = append(aliases, fmt.Sprintf("%s_%s",alias,strings.ReplaceAll(strings.ReplaceAll(skinToneName,"-", "_")," ", "_")))
				}
				currentEmojiWithSkinTone.Aliases = aliases
                // I cleared the unicode version because some emoji with skin tone were added way after their original. I could parse the unicode list,
				// but I'm a loafer, so I did not.
				currentEmojiWithSkinTone.UnicodeVersion = "" 
				// we add the new emoji to the list
                list = append(list, currentEmojiWithSkinTone)
			}
		}
	}
	return list
}

La cl√© 🔑 ici, c’est cette ligne :

currentEmojiWithSkinTone.Emoji = string(append([]rune(currentEmojiWithSkinTone.Emoji), tone...))

Je ne suis pas un expert en Go, encore moins en UTF-8. J’ai donc s√Ľrement rat√© un ou plusieurs trucs, mais apr√®s des heures √† essayer d’afficher mes emojis ligatur√©s avec fmt sans succ√®s (il y avait toujours deux caract√®res d’affich√©s), j’ai fait une conversion de type par inadvertance et √ßa a fonctionn√© ! Je n’ai aucune id√©e de pourquoi j’ai eu besoin de deux heures pour cela.

Nous avons maintenant nos emojis de toutes les couleurs ! 🎉

Résultat de la recherche `ok hand` affichant toutes les variations de couleur de l'émoji de base
R√©sultat de la recherche ok hand affichant toutes les variations de couleur de l’√©moji de base

🚫 emojis incompatibles

Mon ordinateur et mon t√©l√©phone ne supportent pas bien les emojis publi√©s apr√®s la version 14. Mais comme je l’ai dit plus t√īt, la beaut√© des ligatures de l’UTF-8, c’est que malgr√© cela, je vois quand m√™me les diff√©rents composants. De cette fa√ßon, je ne perds pas le sens original.

Plusieurs emojis `couple with heart man man` qui affichent la couleur de peau dans un second caractère, un carré de la couleur
Plusieurs emojis couple with heart man man qui affichent la couleur de peau dans un second caractère, un carré de la couleur

Si vous voulez tester par vous-même et bidouiller le code, vous pouvez trouver le code complet et fonctionnel sur ce repository : git2.riper.fr/ztec/emoji-search-engine-go.

Vous pouvez aussi tester et voir le résulta final. Tous les détails son ici: poulpe.ztec.fr

⁉️ Pourquoi j’ai fait tout √ßa ?

D√©j√†, pourquoi pas ? Juste jouer avec des √©mojis, c’est fun. Mais surtout, mon but √©tait d’avoir un moteur de recherche d’√©mojis sous la main pour pouvoir copier les √©mojis ailleurs. Tous les syst√®mes que j’ai trouv√©s en ligne me semblaient inadapt√©s et p√©nibles √† utiliser.

  • Beaucoup trop lent √† charger et √† chercher.
  • Beaucoup trop inutile. Je ne souhaite pas chercher mon √©mojis dans une liste interminable d’ic√īnes jaunes.

La meilleure solution que j’avais trouv√©e, c’√©tait un raccourci vers le fichier du site Unicode. Mais comme les noms d’usage ne sont pas tous inclus, j’ai d√Ľ apprendre les versions officielles. Puis un jour, le site d’Unicode est tomb√© et n’√©tait plus disponible pendant quelques heures.

Ouais, je dois √™tre le seul au monde qui sait quand le site d’Unicode tombe, et surtout qui est impact√© par √ßa ! 🤣

⏭️ Et la suite ?

Ce moteur est vraiment simple, basique. Il y a plein de fa√ßons de l’am√©liorer. J’ai d’ailleurs d√©j√† inclus une recherche inverse m√™me si je n’en ai pas parl√© ici. Bleve est puissant malgr√© tout, mais rate certains cas √©vidents. Je vais voir ce qui me d√©range le plus et l’am√©liorer en fonction de cela. Peut-√™tre que le r√©sultat sera open-source un jour, mais pour cela je dois encore faire du m√©nage dans mon projet. Les √©mojis ne sont pas les seuls trucs que je “cherche” dans mon moteur de recherche. 😉

Merci infiniment de m’avoir lu,
Bisoux 😗

Bien qu'il ne soit pas possible de commenter sur ce blog, vous pouvez me joindre sur les réseaux sociaux via l'une des publications suivantes que j'ai faites
  • Publi√© le
  • Modifi√© la derni√®re fois le
  • Traduction de cet article disponnible : English
  • Publi√© dans les cat√©gories : Tech - Go - Emoji - Aid√© d'une IA
  • Promotions : Mastodon logo Mastodon Twitter logo Twitter
  • √Ā l'exception des oeuvres cit√©es qui concervent leur droits et attribution originaux, article et son contenu publi√© sous la licence Creative Commons(CC BY-NC-SA 4.0)
  • Si vous avez trouv√© une faute d'ortographe, d'accord, ou une coquille suggerez moi directement une correction via Github.
  • Vous pouvez vous abonn√© √† ce blog par RSS icon RSS pour recevoir les nouveaux articles d√®s leur parution