Intelligente Such-Sortierung mit Vue.JS
Eine search-as-you-type Suche ist mit Vue.js schnell und einfach implementiert. Komplizierter wird es mit der Sortierung möglicher Suchtreffer. Nehmt als Beispiel eine Liste bestehend aus Vor- und Nachnamen, die ihr nach dem Wort Muster durchsucht. Sortiert ihr die Treffer alphabetisch, so bekommt ihr ggf. folgendes Ergebnis:
- Alfred Hamuster
- Erika Mustermann
- Willi Muster
Die Sortierung ist für den Menschen allerdings wenig intuitiv. Wenn ich nach Muster suche, erwarte ich dass Willi Muster ganz oben bei den Treffern steht. Stattdessen steht dort aber Alfred Hamuster, nach dem ich vermutlich eher nicht gesucht habe. Gerade bei sehr langen Ergebnis-Listen kann es störend sein, wenn die relevanten Treffer unten stehen und oben nur irrelevant Einträge sich befinden.
In diesem Blog-Post zeige ich euch, wie ihr eine natürlichere Sortierung der Suchtreffe hinbekommt.
Vue.js
Vue.js ist ein modernes JavaScript-Framework, mit dem sich interaktive Anwendungen vergleichsweise einfach programmieren lassen. Die Grundlagen von Vue.js hat man recht schnell gelernt, z.B. mittels diesem Tutorial: Vue.js Getting started.
Ich persönlich mag Vue.js deutlich lieber als andere JavaScript-Frameworks, wie z.B. React, da sich Vue.js sehr bequem in existenten HTML integrieren lässt. Gerade wenn Teile der Website mit PHP entwickelt sind, kann man Vue.js an den entsprechend notwendigen Stellen schnell und einfach einbinden. Betreibt ihr beispielsweise ein online Casino mit Lastschrift und möchtet gewisse Elemente interaktiv gestalten, so wäre Vue.js die ideale Wahl dafür.
Natürliche Sortierung
Um eine natürlich wirkende Sortierung hinzubekommen, nutze ich ein paar Heuristiken um zu schauen ob ein Treffer relevant als der andere für eine gegeben Suchanfrage ist:
- Prio 1 - Exakte Übereinstimmung: Wenn ein Eintrag identisch zur Suchanfrage ist, soll dies die höchste Priorität haben und ganz oben im Suchergebnis stehen.
- Prio 2 - Wortanfang: Ist die Suche der Wortanfang von einem Eintrag, so erhält es die Prio 2 in der Sortierung.
- Prio 3 - Enthält die Wörter exakt: Enthält ein Eintrag exakt alle Wörter der Suche, so bekommt es die Prio 3 in der Sortierung.
- Prio 4 - Such-Wörter sind Wortanfänge: Die verschiedenen Suchwörter sind die Wortanfänge in dem Eintrag. Für die Suche Muster soll Mustermann vor Hamuster stehen.
Dies sind vier von mir ausgedachte Heuristiken. Ihr könnt natürlich für eure Anwendung andere Heuristiken verwenden, diese um weitere ergänzen etc. Man muss entsprechend immer schauen, was der Anwender als erste Treffer in der Liste erwartet. Vielleicht macht es ja Sinn, dass kürzere Treffer weiter oben stehen? Oder dass Vornamen wichtiger sind als Nachnamen?
Implementierung mittels Vue.Js
Der fertige Code sieht wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Intelligente Sortierung</title> <!-- Vue.js --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <b>Suche</b> <br> <input v-model="search"> <br><br> <b>Ergebnisse:</b> <ul> <li v-for="name in filter_sorted_names">{{name}}</li> </ul> </div> <script> //Eine Hilfsfuntkion, um namen anhand der Scores zu sortieren function arg_sort(namen, scores) { return namen.map((item, index) => { return {name: item, score: scores[index]} }) .sort(function(a, b) { if(a.score != b.score) { return a.score - b.score; } return a.name.localeCompare(b.name); }) .map(item => item.name); } var app = new Vue({ el: '#app', data: { namen: [ 'Alfred Hamuster', 'Paul Mustermann', 'Erika Mustermann', 'Stefan Müller', 'Klaus Schröder', 'Lisa Schmidt', 'Marlene Sonnenschein', 'Muster', 'Willi Muster', ], search: "Muster" }, computed: { filter_sorted_names: function () { //Filtere die Einträge, die den Suchtext enthalten const filtered_names = this.namen.filter(name => { return name.toLowerCase().indexOf(this.search.toLowerCase()) > -1 }); // Weise jedem Eintrag einen Score zu basieren auf den // vier definierten Heuristiken. Je kleiner der Score, desto // höher steht der Treffer im Ergebnis const name_scores = filtered_names.map(function (name) { const name_lower = name.toLowerCase(); const search_lower = this.search.toLowerCase(); //Prio 1: Exakter Match if(name_lower == search_lower) { return 1; } //Prio 2: Beginnt mit Suchwort if(name_lower.startsWith(search_lower)) { return 2; } //Prio 3: Enthält exakt alle Wörter der Suche? const name_words = name_lower.split(" "); const search_words = search_lower.split(" "); let name_contains_all_words = true; for(const search_word of search_words) { if (!name_words.includes(search_word)) { name_contains_all_words = false; } } if(name_contains_all_words) { return 3; } //Prio 4: Sind alle Wörter der Suche die Wortanfänge des Eintrags? let name_starts_with_words = true; for(const search_word of search_words) { if(!name_words.some(word => word.startsWith(search_word))) { name_starts_with_words = false; } } if(name_starts_with_words) { return 4; } //Default Wert return 1000; }.bind(this)); //Sortiere die Treffer basierend auf den Scores return arg_sort(filtered_names, name_scores); } } }) </script> </body> </html> |
Zuerst filtern wird die Suchergebnisse:
1 2 3 4 |
//Filtere die Einträge, die den Suchtext enthalten const filtered_names = this.namen.filter(name => { return name.toLowerCase().indexOf(this.search.toLowerCase()) > -1 }); |
und behalten nur die Einträge, die den Suchstring enthalten. Groß- und Kleinschreibung wird dabei ignoriert. Man könnte das Filtern hier auch besser gestalten, indem man die Suchanfrage aufteilt in einzelne Wörter und schaut ob jedes Wort im Eintrag vorhanden ist.
Anschließend weisen wir jedem Namen einen Score zu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// Weise jedem Eintrag einen Score zu basieren auf den // vier definierten Heuristiken. Je kleiner der Score, desto // höher steht der Treffer im Ergebnis const name_scores = filtered_names.map(function (name) { const name_lower = name.toLowerCase(); const search_lower = this.search.toLowerCase(); //Prio 1: Exakter Match if(name_lower == search_lower) { return 1; } //Prio 2: Beginnt mit Suchwort if(name_lower.startsWith(search_lower)) { return 2; } //Prio 3: Enthält exakt alle Wörter der Suche? const name_words = name_lower.split(" "); const search_words = search_lower.split(" "); let name_contains_all_words = true; for(const search_word of search_words) { if (!name_words.includes(search_word)) { name_contains_all_words = false; } } if(name_contains_all_words) { return 3; } //Prio 4: Sind alle Wörter der Suche die Wortanfänge des Eintrags? let name_starts_with_words = true; for(const search_word of search_words) { if(!name_words.some(word => word.startsWith(search_word))) { name_starts_with_words = false; } } if(name_starts_with_words) { return 4; } //Default Wert return 1000; }.bind(this)); |
Hier könnt ihr beliebige weitere Sortierregeln implementieren, z.B. könnten kurze Treffer bevorzugt werden und eine geringere Priorität bekommen.
Zum Schluss nutzen wir noch eine Hilfsfunktion, die die Namen basierend auf den Prioritäten sortiert:
1 2 3 4 5 6 7 8 9 10 11 |
//Eine Hilfsfuntkion, um namen anhand der Scores zu sortieren function arg_sort(namen, scores) { return namen.map((item, index) => { return {name: item, score: scores[index]} }) .sort(function(a, b) { if(a.score != b.score) { return a.score - b.score; } return a.name.localeCompare(b.name); }) .map(item => item.name); } |
Fertiges npm Package - match-sorter
Wer sich nicht den Aufwand machen möchte dies selber zu implementieren, dem kann ich das fertige Package match-sorter empfehlen. Dies lässt sich per npm installieren und nutzt ebenfalls verschiede Methoden, um Treffer besser (natürlicher) zu sortieren:
Autor: Nils Reimers