Stoppwörter mit LINQ

21. Januar 2014

Kleine aber feine Problemlösungen machen Spaß und immer öfters ist – zumindest bei mir – LINQ daran beteiligt. Heute war es die Problemstellung der “Stoppwörter”, die mir wieder einmal zeigten, wie gut das Konzept der LINQ-Abfragen funktioniert. 

Die Aufgabenstellung besteht darin, aus einer Liste von Strings diejenigen ohne Stoppwörter herauszufinden.

Beispiel:

Wortliste : "Das", "ist", "ein Stop1 mit", "ein", "-em Stop2 als", "Stopptext"

Stoppliste: "Stop1", "Stop2"

Ergebnis  : "Das", "ist", "ein", "Stopptext"

Die offensichtliche Lösung mit zwei verschachtelten Schleifen ist in diesem Fall nicht besonders schwer zu schreiben. Ein Blick auf die Problemstellung zeigt, dass zwei Mengen miteinander verarbeitet werden, was für mich ein guter Indikator für den Einsatz von LINQ ist.

LINQ 2 Objects

LINQ 2 Objects führt Abfragen auf Objekten im Speicher aus. Die Lösung für das Problem sieht dabei wie folgt aus:

Suche in der Wortliste words.Where()
Wörter, l =>
für die in der Stoppliste kein Stoppwort s =>
gefunden werden kann !stopWords.Where(…).Any()
das im Wort enthalten ist l.Contains(s)

   1: var words = new List<string> { 

   2:   "Das", "ist", "ein Stop1 mit", "ein", "-em Stop2 als", "Stopptext"};

   3: var stopWords = new List<string> { "Stop1", "Stop2" };

   4:  

   5: // Nur Texte ohne Stoppwörter ermitteln

   6: var result = words.Where(

   7:     l => !stopWords.Where(s => l.Contains(s)).Any()

   8: );

 
Eine gute Kommentierung von komplexen LINQ-Statements ist unumgänglich. “It was hard to write so it should be hard to read” ist kein verlässliches Prinzip in der Softwareentwicklung.

LINQ to Entities

LINQ to Entities kann über das Entity Framework Abfragen an den SQL-Server weitergeben, so dass sie dort ausgeführt werden und den Client entlasten. Ich war gespannt wie LINQ to Entities die Abfrage in SQL umsetzt. Hierzu habe ich zwei Tabellen “Words”, “StopWords” erzeugt, die jeweils die Wörter und Stoppwörter enthalten. Die Abfrage wird jeweils um die Selektion auf die Wort-Spalte erweitert:

   1: var dbWords = db.Words.Select(w => w.Word);

   2: var dbStopWords = db.StopWord.Select(w => w.StopWord1);

   3: var result = dbWords.Where(

   4:     l => !dbStopWords.Where(s => l.Contains(s)).Any()

   5: );

Das Ergebnis setzt LINQ to Entities perfekt um

   1: SELECT 

   2: [Extent1].[Word] AS [Word]

   3: FROM [dbo].[Words] AS [Extent1]

   4: WHERE  NOT EXISTS (SELECT 

   5:     1 AS [C1]

   6:     FROM [dbo].[StopWord] AS [Extent2]

   7:     WHERE (CHARINDEX([Extent2].[StopWord], [Extent1].[Word])) > 0

   8: )

Anmerkung: Ein Index auf die Wörter-Spalten ist für eine brauchbare Performance Pflicht.

Fazit

Zwei verschachtelte Schleifen mit Vergleichen und Zuordnungen können mit einem LINQ-Einzeiler ersetzt werden und ich habe mit Zufriedenheit festgestellt, dass LINQ to Entities die Anfragen hervorragend in SQL umsetzt.

Somit hoffe ich, dass zukünftig LINQ noch viel mehr eingesetzt wird. Es führt in der Regel zu kurzem und verständlichen Code.