Unscharfe Suche – Sebastians Blog https://sgaul.de Neues aus den Softwareminen Sat, 06 Jul 2013 12:28:21 +0000 de-DE hourly 1 https://wordpress.org/?v=6.1.7 https://sgaul.de/wp-content/uploads/2019/02/cropped-sgaul-2-1-32x32.jpg Unscharfe Suche – Sebastians Blog https://sgaul.de 32 32 Fuzzy-Search-Filter für die Bash https://sgaul.de/2013/07/07/fuzzy-search-filter-fur-die-bash/ Sun, 07 Jul 2013 13:24:33 +0000 https://sgaul.de/?p=2265 Fuzzy-Search-Filter für die Bash weiterlesen]]> Ich hatte kürzlich von meiner nicht hunderprozentig zufriedenstellenden Suche nach einer Fuzzy-Search für die Bash geschrieben. Das Problem ist nicht das Herausfiltern an sich, sondern die Gewichtung der Ergebnisse. Als Maß für die Güte eignet sich die Levenshtein-Distanz. Diese zählt die Notwendigen Ersetzungen, Einfügungen und Löschungen, die nötig sind, um String a in String b zu überführen. Je geringer diese sogenannte Distanz, desto ähnlicher sind sich die Strings und desto passender sind sie als Suchergebnis (zumindest in der Theorie).

Meine Suche nach einer einfachen Implementierung, die ich von der Bash aufrufen kann, ist leider erfolglos geblieben. Interessanterweise ist die Implementierung der Levenshtein-Distanz für eine Fuzzy-Search sehr einfach, so dass es sich auch in sperrigem Bash gut umsetzen lässt.

Nur Einfügungen = einfache Levenshtein-Distanz

Die Vereinfachung basiert auf der Tatsache, dass bei meiner Fuzzy-Search nur eingefügt, nie aber ein Zeichen gelöscht oder verändert wird. Suchen wir unscharf nach abc, so ergeben sich beide Strings als Treffer:

cat $file_to_be_filtered
xxxaxxxbxxxcxxx
yyyaybcyyy

Im nächsten Schritt schneiden wir alles weg, was nicht benötigt wird, um den String matchen zu lassen (hier muss non-greedy gesucht werden, um den kleinst-möglichen Treffer zu finden):

... | grep -oP .*?a.*?b.*?c.*?
axxxbxxxc
aybc

Nun zählen wir die verbliebene Zeichenlänge und ziehen die Länge des Fuzzy-Suchstrings (|abc| = 3) ab und ermitteln somit die Levenshtein-Distanz.

axxxbxxxc # 9 - 3 = 6
aybc      # 4 - 3 = 1

Die Länge des Suchstrings ist konstant und hat auf die Ordnung keinen Einfluss. Sie kann in der Implementierung weggelassen werden.

... | awk '{ print length }' | sort | head -1

Implementierung in Bash

Hier mal eine einfache Umsetzung, die eine Datei unscharf durchsucht und die Ergebnisse nach aufsteigender Levenshtein-Distanz ausgibt. Der Algorithmus arbeitet mit einer temporären Datei, da sich diese in meinen Augen sehr viel einfacher bearbeiten lassen als Arrays oder ähnliches. Da die Datei nur virtuell im Arbeitsspeicher angelegt wird, sollte dies aber kein Problem darstellen.

#!/bin/bash

file_to_be_filtered="$1"
filter_string="$2"
# use in-memory directory /dev/shm/
result_file=/dev/shm/.fuzzyfilterresults

if [ ! -f $file_to_be_filtered ]; then
	echo "fuzzy_filter: '$file_to_be_filtered': No such file"
	exit 1
fi

# empty result file
> $result_file
# replace abc with .*?a.*?b.*?c.*?
# ? triggers non-greedy search in Perl
fuzzy_filter_string=$(echo "$filter_string" | sed 's/./&.*?/g')

while read line; do
	# get matches only (grep -o) with Perl syntax (-P)
	# replace them by their string length (awk)
	# return the smallest number (sort | head -1)
	match_length=$(echo "$line" | grep -oP "$fuzzy_filter_string" | awk '{ print length }' | sort | head -1)
	# check if variable is a number and not 0
	if [ "$match_length" -eq "$match_length" ] 2>/dev/null && [ "$match_length" -gt "0" ]; then
		# write number, space and original content into result file
		line="$match_length $line"
		echo $line >> $result_file
	fi
done < "$file_to_be_filtered"

# sort by facing order number
sort $result_file -o $result_file
# cut order number and first space, output the rest
cut -d " " -f 2- $result_file

rm -f $result_file
]]>
Die Suche nach der Fuzzy-Search https://sgaul.de/2013/06/30/die-suche-nach-der-fuzzy-search/ https://sgaul.de/2013/06/30/die-suche-nach-der-fuzzy-search/#comments Sun, 30 Jun 2013 12:11:22 +0000 https://sgaul.de/?p=2252 Die Suche nach der Fuzzy-Search weiterlesen]]> Unscharfes Suchen ist äußerst hilfreich. Wer länger mit Sublime oder vergleichbaren Editoren gearbeitet hat weiß es zu schätzen, dass er statt eines Datei-Dialogs Strg+p, „filibpost“ und Enter drückt, um etwa files/sql/libs/postgresql-lib.rb zu öffnen. Ich arbeite gerade an einem ähnlichen Hilfskommando, welches das Öffnen in Vim etwa mittels ff filibpost vim ermöglicht. Alles nicht so schwierig – bis auf die sogenannte „Fuzzy Search“.

Zunächst mein (nicht wirklich optimales) Testszenario:

$ tree
.
└── files
    └── sql
        └── libs
            ├── db2-lib.rb
            ├── mariadb-lib.rb
            ├── mysql-lib.rb
            └── postgresql-lib.rb

find + grep

Mit find . -type f werden alle Dateien des aktuellen Verzeichnisses und aller Unterverzeichnisse aufgelistet. Diese werden dann auf den Suchstring überprüft. Da Grep nicht „unscharf“ ist, wird der Suchstring „abc“ mittels $(echo "$1" | sed 's/./&.*/g') in „*a*b*c*“ umgewandelt.

function grep_search {
  fuzzy_search_string=$(echo "$1" | sed 's/./&.*/g')
  find . -type f | grep -i "$fuzzy_search_string"
}

Schwachstellen

Diese Lösung funktioniert und ist auch schnell genug, allerdings sind die Ergebnisse nicht gewichtet. Wenn ich nach „bilderfamilie“ suche, treffen auch sehr tiefe Hierarchien schnell zu. Und wenn sich mein gewünschtes Ergebnis zwischen hunderten Dateien wie ./Bilder/Icons/Faenza/c/22/media-optical-audio-new.png steckt, wird das Herauspicken aufwendig.

find + agrep -p

Dann habe ich über Agrep gelesen, was zunächst vielversprechend klang. Das Tool ist mächtig, aber auch entsprechend etwas komplizierter. Die Option -p war in den Man-Pages jedoch vielversprechend dokumentiert:

agrep -p DCS foo will match „Department of Computer Science.“

function agrep_search {
  find . -type f | agrep -pi $1
}

Schwachstellen

Um es kurz zu machen: Das Ergebnis ist das selbe wie bei dem selbst zusammengesetztem Grep.

find + agrep -By

Okay, Ziel verfehlt. Die „besten“ Treffer sind immer noch nicht oben. Aber dann:

Best match mode. When -B is specified and no exact matches are found, agrep will continue to search until the closest matches (i.e., the ones with minimum number of errors) are found…

Genau was ich will. Wird kein exakter Treffer gefunden, sucht man einen mit einem, dann mit zwei, dann mit drei Fehlern. Klingt gut.

Die Option -B kann nicht vom Stdin lesen (sie muss ja mehrfach durchgehen), daher die etwas andere Implementierung:

function agrep_by_search {
  find . -type f > $HOME/.agrepresults
  agrep -By -S8 -D8 -i $1 $HOME/.agrepresults
  rm $HOME/.agrepresults
}

Schwachstellen

Diese Methode findet nur die besten Lösungen, also die, die mit den wenigsten Fehlern passen. Passt die gesuchte Datei mit drei Fehlern, eine andere aber schon mit zwei, wird die gesuchte nicht angezeigt. Dies ließe sich beheben, in dem man die Funde aus der Zwischenspeicherdatei löscht und die Suche erneut startet, bis die Ergebnisliste mindestens x Einträge hat.

Ein größeres Problem ist aber, dass diese Suchmethode alle Fehlerarten zu berücksichtigen scheint: Einfügung, Ersetzung und Löschung. In einer unscharfen Suche sollten jedoch nur Einfügungen erlaubt sein. Das Heraufsetzen der Kosten von Deletion und Substitution mittels -S8 -D8 scheint leider keinen Unterschied zu machen. So zeigt die Suche nach „mlib“ im obigen Dateisystembeispiel auch Pfade an, die kein m enthalten (siehe folgende Tabelle).

Beispiel-Suchanfragen

grep mit sed ’s/./&.*/g‘ agrep -pi agrep -By
sql
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/postgresql-lib.rb
./files/sql/libs/db2-lib.rb
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/postgresql-lib.rb
./files/sql/libs/db2-lib.rb
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/postgresql-lib.rb
./files/sql/libs/db2-lib.rb
mlib
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/postgresql-lib.rb
./files/sql/libs/db2-lib.rb
sql-lib
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/postgresql-lib.rb
./files/sql/libs/db2-lib.rb
./files/sql/libs/mysql-lib.rb
./files/sql/libs/mariadb-lib.rb
./files/sql/libs/postgresql-lib.rb
./files/sql/libs/db2-lib.rb
./files/sql/libs/mysql-lib.rb
./files/sql/libs/postgresql-lib.rb

Die Suche geht also weiter…

]]>
https://sgaul.de/2013/06/30/die-suche-nach-der-fuzzy-search/feed/ 4