Dieser Artikel wurde maschinell übersetzt.

Vorhersage: „Wolkig“

Durchsuchen des Windows Azure-Speichers mit Lucene.Net

Joseph Fultz

Downloaden des Codebeispiels

Joseph FultzSie wissen, was Sie brauchen, ist in Ihren Daten irgendwo in der Wolke begraben, aber Sie nicht wissen, wo. In diesem Fall so oft zu so vielen und in der Regel die Antwort Suche in zwei Arten implementiert ist. Am einfachsten ist-Metadaten in einer SQL Azure-Datenbank und verwenden Sie eine WHERE-Klausel finden Sie den Uri auf ein LIKE-Abfrage die Daten basieren. Dies hat sehr offensichtliche Mängel, z. B. Einschränkungen in übereinstimmenden anhand der wichtigsten Metadaten statt der Inhalt des Dokuments, potenzielle Probleme mit der Größe der Datenbank für SQL Azure, Premium-Kosten zum Speichern von Metadaten in SQL Azure und der Aufwand bei der Erstellung eine spezialisierte Indexmechanismus oft als Teil der Persistenzebene implementiert hinzugefügt. Zusätzlich zu diesen Mängel sind spezialisiertere Suchfunktionen, die einfach, wie z. B. werden nicht:

  • Nach Relevanz
  • Sprache Tokenisierung
  • Ausdruck übereinstimmenden und knappe Übereinstimmung
  • Wortstammerkennung und übereinstimmende synonym

Ein zweiter Ansatz habe ich gesehen den Cloud-Inhalt aus der lokalen Suchindizierung indiziert ist, und dies hat seine eigenen Probleme. Entweder das Dokument lokal indiziert ist, und Uri behoben, bis die Komplexität der Persistenz und Indizierung, da die Datei lokal sein muss und in der Wolke führt; oder der lokale Indexdienst erreicht die Wolke, die Leistung verringert und erhöht die Bandbreitenauslastung und Kosten. Darüber hinaus könnte mit Ihrer lokalen Suchmaschine erhöhte Lizenzkosten, sowie bedeuten. Ich habe eine Mischung der beiden Ansätze, die unter Verwendung eines lokalen SQL Server und Full-Text Indexing.

Theoretisch bei SQL Azure Full-Text fügt Indizierung, Sie werden bei der erste Methode zufriedenstellend mehr verwenden, aber es wird noch eine angemessene Menge von Speicherplatz in der Datenbank für die indizierte Inhalte erfordern. Ich wollte das Problem beheben und die folgenden Kriterien erfüllen:

  1. Halten Sie die Kosten im Zusammenhang mit der Lizenzierung, Speicherplatz und niedriger Bandbreite.
  2. Haben Sie eine echte Suche (nicht Pressen Draht- und Rohr Band umwickelt SQL Server).
  3. Entwerfen und Implementieren einer Architektur, die analog zu was ich im Unternehmens-Datencenter implementiert werden können.

Architektur mit Lucene.Net suchen

Soll eine echte Sucharchitektur, daher benötigen eine echte Indizierung und search Engine. Glücklicherweise viele andere wollte das gleiche und erstellt eine schöne.NET-Version der open-Source-Lucene-Suche und Indizierung Bibliothek Sie unter finden incubator.apache.org/lucene.net. Tom Laird-McConnell erstellt außerdem eine fantastische Bibliothek für Lucene.Net mit Windows Azure Speicher; finden sie unter code.msdn.microsoft.com/AzureDirectory. Mit diesen zwei Bibliotheken brauche ich nur zum Schreiben von Code Crawl und Index des Inhalts und einen Suchdienst, um den Inhalt zu finden. Die Architektur wird imitieren typischen Sucharchitektur mit Index-Speicher, einen Indexdienst, Search-Dienst und einige Front-End-Webserver verbrauchen den Search-Dienst (siehe Abbildung 1).

Search Architecture

Abbildung 1 Suche-Architektur

Die Bibliotheken Lucene.Net und AzureDirectory werden eine Worker-Rolle, die als der Indexdienst ausgeführt, aber die Front-End-Web-Rolle nur muss den Search-Dienst zu nutzen und nicht die suchen-spezifischen Bibliotheken. Konfigurieren von Speicher- und Compute-Instanzen in derselben Region sollten die Bandbreitenauslastung halten – und Kosten – während der Indizierung und Suche nach unten.

Das Crawlen und Indizieren

Der Worker-Rolle ist verantwortlich für die Dokumente im Speicher crawlen und Indizieren diese. Ich habe den Bereich nur Word DOCX-Dokumente, behandeln eingegrenzt mithilfe von OpenXML SDK 2.0, verfügbar unter msdn.microsoft.com/library/bb456488. Ich habe beschlossen, die neueste Codeversion von für AzureDirectory und Lucene.Net tatsächlich in meinem Projekt, sondern nur verweisen auf die Bibliotheken einfügen.

Innerhalb der Run-Methode ich einen vollständigen Index am Anfang und dann Feuer eine inkrementelle Aktualisierung innerhalb der Sleep-Schleife, die Einrichtung, etwa so:

Index(true);

while (true)
{
  Thread.Sleep(18000);
  Trace.WriteLine("Working", "Information");

    Index(false);
}

Für mein Beispiel halte ich die Schleife in einem angemessenen Intervall von 18.000 ms schlafen gehen. Ich habe keine Methode für das Auslösen eines Indexes erstellt, aber es wäre leicht genug, um einen einfachen Dienst zum Auslösen dieser gleichen Index-Methode bei Bedarf manuell Auslösen einer Aktualisierung des Index aus einer Administratorkonsole oder rufen Sie es von einem Prozess, der Aktualisierungen und Ergänzungen der Speichercontainer überwacht hinzufügen. In jedem Fall soll noch geplanten durchsuchen und diese Schleife als eine einfache Implementierung von es dienen könnte.

Innerhalb der Index(bool)-Methode sollten Sie zuerst, ob der Index vorhanden ist. Wenn ein Index vorhanden ist, wird nicht ich einen neuen Index generieren, da es die alte Version, die dies bedeuten würde eine Reihe von unnötige Arbeit zu tun Auslöschen würde, da es eine vollständige Indizierung ausgeführt zwingen würde:

DateTime LastModified = new DateTime(IndexReader.LastModified(azureDirectory),DateTimeKind.Utc);
bool GenerateIndex = !IndexReader.IndexExists(azureDirectory) ;
DoFull = GenerateIndex;

Sobald die Bedingungen des Indexes ausgeführt, muss ich öffnen Sie den Index und einen Verweis auf den Container, der indizierten Dokumente abrufen. Ich bin Umgang mit einem einzelnen Container und keine Ordner, aber in einer Produktionsimplementierung würde ich erwarten, mehrere Container und Unterordner. Dies würde erfordern, dass ein bisschen Schleifen und Rekursion sollte, aber dieser Code einfach genug, um Sie später hinzufügen:

// Open AzureDirectory, which contains the index
AzureDirectory azureDirectory = new AzureDirectory(storageAccount, "CloudIndex");

// Loop and fetch the information for each one.
// This needs to be managed for memory pressure, 
// but for the sample I'll do all in one pass.
IndexWriter indexWriter = new IndexWriter(azureDirectory, new StandardAnalyzer(Lucene.Net.Util.
Version.LUCENE_29), GenerateIndex, IndexWriter.MaxFieldLength.UNLIMITED);

// Get container to be indexed.
CloudBlobContainer Container = BlobClient.GetContainerReference("documents");
Container.CreateIfNotExist();

Verwenden die AzureDirectory-Bibliothek mich einen Windows Azure Storage-Container als Verzeichnis für den Lucene.Net-Index zu verwenden, ohne eigenen Code schreiben können, kann der so Schwerpunkt ausschließlich auf den Code für das Crawlen und indizieren. In diesem Artikel werden die beiden interessantesten Parameter im Konstruktor IndexWriter den Analyzer und das GenerateIndex-Flag. Der Analyzer ist verantwortlich für die Daten an den IndexWriter übergeben, es Tokenerstellung und Notizen Erstellen des Indexes. GenerateIndex ist wichtig, da Wenn nicht ordnungsgemäß festgelegt wird der Index jedes Mal überschrieben abrufen und viel Abwanderung verursachen. Bevor er den Code, der die Indizierung wird, definieren Sie ein einfaches Objekt, halten den Inhalt des Dokuments:

public class DocumentToIndex
{
  public string Body;
  public string Name;
  public string Uri;
  public string Id;
  }

Wie ich den Container durchlaufen, jeden Verweis Blob und erstellen eine analoge DocumentToIndex-Objekt für it wurde. Bevor Sie das Dokument zum Index hinzufügen, prüfe ich, ob es geändert wurde, seit der letzten Indizierung durch Vergleichen der Last-modified Zeit, LastModified Zeit des Indexes, der ich zu Beginn des Indexes ausgeführt ergriff ausführen. Ich wird es auch index, wenn das DoFull-Flag auf True festgelegt ist:

foreach (IListBlobItem currentBlob in Container.ListBlobs(options))
{
  CloudBlob blobRef = Container.GetBlobReference(currentBlob.Uri.ToString());
  blobRef.FetchAttributes(options);
  // Add doc to index if it is newer than index or doing a full index
  if (LastModified < blobRef.Properties.LastModifiedUtc || DoFull )
{
    DocumentToIndex curBlob = GetDocumentData(currentBlob.Uri.ToString());

    //docs.Add(curBlob);

    AddToCatalog(indexWriter, curBlob);
  }
}

Für dieses einfache Beispiel prüfe ich, dass die Uhrzeit der letzten Änderung des Index kleiner als die des Dokuments ist, und es gut genug funktioniert. Es gibt Raum für Fehler, da der Index über Optimierung aktualisiert wurden haben konnte und dann es aussehen würde, wie der Index neuer ist als ein bestimmtes Dokument war, jedoch das Dokument wurde nicht indiziert. Ich bin diese Möglichkeit vermeiden, indem einfach einen Optimierung Aufruf mit einer vollständigen Index ausführen. Beachten Sie, dass in einer realen Implementierung würde dieser Entscheidung zu überdenken. Innerhalb der Schleife rufe ich GetDocumentData zum Abrufen des BLOBs und AddToCatalog hinzufügen, die Daten und Felder, die dem Index Lucene.Net interesse. In GetDocumentData verwende ich Recht typischen Code zum Abrufen des BLOBs und einige Eigenschaften für meine darstellendes Objekt festgelegt:

// Stream stream = File.Open(docUri, FileMode.Open);
var response = WebRequest.Create(docUri).GetResponse();
Stream stream = response.GetResponseStream();

// Can't open directly from URI, because it won't support seeking
// so move it to a "local" memory stream
Stream localStream= new MemoryStream();
stream.CopyTo(localStream);

// Parse doc name
doc.Name = docUri.Substring(docUri.LastIndexOf(@"/")+1);
doc.Uri = docUri;

Den Körper ist ein wenig mehr Arbeit. Hier ich eine Switch-Anweisung für die Erweiterung und anschließend mithilfe von OpenXml, ziehen den Inhalt der .docx heraus einrichten (siehe Abbildung 2). OpenXml muss einen Stream, der Operationen, suchen kann, so dass ich den Antwortstream direkt verwenden können. Damit es funktioniert, ich den Antwortstream in einen Speicherstream kopieren und verwenden den Arbeitsspeicherstream. Notieren Sie sich dieser Vorgang, denn wenn außergewöhnlich große Dokumente sind dies theoretisch Probleme verursachen könnte, indem Speicherdruck auf der Arbeitnehmer und etwas ausgefallenere Handhabung des BLOBs erfordern würde.

Abbildung 2 herausziehen, den Inhalt der .docx-Datei

switch(doc.Name.Substring(doc.Name.LastIndexOf(".")+1))
{
  case "docx":
    WordprocessingDocument wordprocessingDocument =
      WordprocessingDocument.Open(localStream, false);
    doc.Body = wordprocessingDocument.MainDocumentPart.Document.Body.InnerText;
    wordprocessingDocument.Close();
        break;
  // TODO:  Still incomplete
  case "pptx":
    // Probably want to create a generic for DocToIndex and use it
    // to create a pptx-specific that allows slide-specific indexing.
PresentationDocument pptDoc = PresentationDocument.Open(localStream, false);
    foreach (SlidePart slide in pptDoc.PresentationPart.SlideParts)
    {
      // Iterate through slides
    }
    break;
  default:
    break;

}

Meine zusätzliche Stub und Kommentare anzeigen, wo den Code zum Behandeln der anderen Formate platziert. In einer Produktionsimplementierung würde ich ziehen Sie den Code für jeden Dokumenttyp und steckte es in eine separate Bibliothek der Dokument-Adapter, dann verwenden die Konfiguration und Dokumentprüfung den Dokumenttyp der richtige Adapter-Bibliothek aufgelöst. Hier ich es platziert haben rechts in der Switch-Anweisung.

Jetzt zu AddToCatalog, um es in den Index gefüllte DocumentToIndex-Objekt übergeben werden kann (Abbildung 3).

Abbildung 3 DocumentToIndex AddToCatalog-Methode übergeben

public void AddToCatalog(IndexWriter indexWriter, DocumentToIndex currentDocument
  )
{

  Term deleteTerm = new Term("Uri", currentDocument.Uri);

  LuceneDocs.Document doc = new LuceneDocs.Document();
  doc.Add(new LuceneDocs.Field("Uri", currentDocument.Uri, LuceneDocs.Field.Store.YES,
    LuceneDocs.Field.Index.NOT_ANALYZED, LuceneDocs.Field.TermVector.NO));
  doc.Add(new LuceneDocs.Field("Title", currentDocument.Name, LuceneDocs.Field.Store.YES,
    LuceneDocs.Field.Index.ANALYZED, LuceneDocs.Field.TermVector.NO));
  doc.Add(new LuceneDocs.Field("Body", currentDocument.Body, LuceneDocs.Field.Store.YES,
    LuceneDocs.Field.Index.ANALYZED, LuceneDocs.Field.TermVector.NO));

  indexWriter.UpdateDocument(deleteTerm, doc);
}

Ich beschloss, drei Felder indizieren: Titel, Uri und Körper (der tatsächliche Inhalt). Beachten Sie, dass für Titel und Textkörper ich das ANALYZED-Flag verwenden. Dies weist Analyzer Token den Inhalt und die Token zu speichern. Dies vor allem für den Körper tun soll, oder Mein Index vergrößert, um größer als die Dokumente, die kombiniert werden. Beachten Sie, die der Uri auf NOT_ANALYZED festgelegt ist. Ich möchte dieses Feld direkt im Index gespeichert, da es einen eindeutigen Wert ist, von dem ich ein bestimmtes Dokument abrufen können. In der Tat verwende es in dieser Methode erstellen Sie einen Begriff (ein Konstrukt verwendet für Suchen Dokumente), die die UpdateDocument-Methode die IndexWriter übergeben wird. Andere Felder, die ich, ob eine Dokumentvorschau unterstützen oder facettierten suchen (z. B. ein Author-Feld) unterstützt, würde ich hier hinzufügen und entscheiden, ob den Text Token basierend auf wie ich planen, verwenden Sie das Feld den Index hinzufügen.

Implementieren des Search-Dienstes

Sobald hatte den Indexdienst gehen, und die Segment-Dateien in den Index-Container konnte sehen, war ich gespannt zu sehen, wie gut es funktioniert. Ich geknackt Öffnen der IService1.cs-Datei für den Suchdienst und nahm einige Änderungen an den Schnittstellen und Verträge. Da diese Datei SOAP-Dienste standardmäßig generiert, beschlossen mit denen für die erste Bahn bleiben werden. Einen Rückgabetyp, das für die Suchergebnisse erforderlich sind, aber den Dokumenttitel und Uri waren genug vorerst, so dass ich eine einfache Klasse als die DataContract definiert:

[DataContract]
public class SearchResult
{
  [DataMember]
  public string Title;
  [DataMember]
  public string Uri;
}

Mithilfe des SearchResult-Typs, definiert eine einfache Suche-Methode als Teil der ISearchService ServiceContract:

[ServiceContract]
public interface ISearchService
{

  [OperationContract]
  List<SearchResult> Search(
    string SearchTerms);

Als Nächstes SearchService.cs geöffnet und eine Implementierung für den Suchvorgang hinzugefügt. Erneut AzureDirectory ins Spiel kommt, und ich instanziiert eine neue aus der Konfiguration der IndexSearcher-Objekt übergeben. Die AzureDirectory-Bibliothek bietet nicht nur eine Directory-Schnittstelle für Lucene.Net, fügt auch eine intelligente Schicht der Dereferenzierung, die zwischengespeichert und komprimiert. AzureDirectory-Vorgänge erfolgen lokal und Schreibvorgänge werden verschoben, um Speicher auf wird festgeschrieben, mit der Komprimierung, um Latenz und Kosten zu reduzieren. An dieser Stelle kommen eine Anzahl von Lucene.Net Objekten ins Spiel. Die IndexSearcher dauert eine Abfrage-Objekt und im Index. Ich habe jedoch nur eine Reihe von Bedingungen, die als Zeichenfolge übergeben. Diese Begriffe in einer Abfrage-Objekt zu erhalten, habe ich eine QueryParser verwenden. Ich muss sagen, welche Felder übernehmen Sie die Bedingungen für die QueryParser und Bedingungen zu bieten. In dieser Implementierung bin ich nur den Inhalt des Dokuments suchen:

// Open index
AzureDirectory azureDirectory = new AzureDirectory(storageAccount, "cloudindex");
IndexSearcher searcher = new IndexSearcher(azureDirectory);

// For the sample I'm just searching the body.
QueryParser parser = new QueryParser("Body", new StandardAnalyzer());
Query query = parser.Parse("Body:(" + SearchTerms + ")");
Hits hits = searcher.Search(query);

Wenn ich eine facettierte Suche bereitstellen wollte, würde muss ein Mittel, wählen das Feld, und erstellen Sie die Abfrage für das Feld implementiert, aber ich hätte der Index im vorherigen Code hinzufügen, an dem Waren Titel, Uri und den Text hinzugefügt. Nur noch im Dienst jetzt zu tun ist, durchlaufen die Zugriffe und Auffüllen der Liste zurück:

for (int idxResults = 0; idxResults < hits.Length(); idxResults++)
{
  SearchResult newSearchResult = new SearchResult();
  Document doc = hits.Doc(idxResults); 

  newSearchResult.Title  = doc.GetField("Title").StringValue();
  newSearchResult.Uri = doc.GetField("Uri").StringValue();
  retval.Add(newSearchResult);
}

Denn ich bin ein bisschen ungeduldig, ich möchte nicht warten, um die Web-front-End zu testen, zu beenden, damit ich das Projekt auszuführen, starten Sie WcfTestClient, fügen einen Verweis auf den Dienst und die Suche nach dem Begriff "Cloud" (siehe Abbildung 4).

WcfTestClient Results

Abbildung 4 WcfTestClient Ergebnisse

Ich bin sehr glücklich, es wieder mit den erwarteten Ergebnissen kommen zu sehen.

Beachten Sie, dass während den Search-Dienst ausführen und Indizierung Rollen aus der Windows Azure-Emulator zu berechnen, ich tatsächlich Windows Azure Speicher verwende.

Suchseite

Wechseln zu Mein Front-End-Web-Rolle, mache ich einige schnellen Änderungen an der Seite default.aspx ein Textfeld und einige Beschriftungen hinzufügen.

Als Abbildung 5 zeigt die wichtigste Änderung des Aufschlags wird im Datenraster, in dem datengebundenen Spalten für Titel und dem Uri, der im Resultset verfügbar sein sollte definieren.

Defining Databound Columns for the Data Grid

Abbildung 5 datengebundene Spalten für das Datenraster definieren

Ich hinzufügen schnell, einen Verweis auf die Suche Service-Projekt zusammen mit ein wenig Code hinter der Schaltfläche Suchen, rufen Sie den Search-Dienst und die Ergebnisse an das Datagrid gebunden:

protected void btnSearch_Click(
    object sender, EventArgs e)
  {
    refSearchService.SearchServiceClient  
      searchService = new
      refSearchService.SearchServiceClient();
    IList<SearchResult> results = 
      searchService.Search(
        txtSearchTerms.Text);
    gvResults.DataSource = results;
    gvResults.DataBind();
  }

Einfach genug, damit mit einem schnellen Hahn F5 ich zu sehen, was ich erhalte einen Suchbegriff eingeben. In Abbildung 6 werden die Ergebnisse angezeigt. Wie erwartet zurückgegeben eingeben "Neudesic" zwei Hits aus verschiedenen Dokumente, die ich bereitgestellt hatte, im Container.

Search Results

Abbildung 6 Suchergebnissen

Schlussbemerkung

Ich behandeln nicht die Art der erweiterte Themen, die Sie implementieren, wenn der Katalog und die zugehörige Index groß genug wachsen können. Mit Windows Azure, Lucene.Net und eine Prise OpenXML können jeder suchen Anforderungen erfüllt werden. Da es nicht viel Unterstützung für eine Lösung für die Suche Cloud bereitgestellt noch, vor allem eine, die eine benutzerdefinierten Sicherheitsimplementierung über Windows Azure Speicher konnte, möglicherweise Lucene.Net die beste Option gibt an, wie es die Anforderungen der Implementierer gebogen werden kann.

Joseph Fultz ist Softwarearchitekt bei AMD, zum Definieren der allgemeinen Architektur und Strategie für das Portal und Services-Infrastruktur und Implementierungen unterstützen. Zuvor war er ein Softwarearchitekt für Microsoft und arbeitet mit Top-Tier-Unternehmen und ISV-Kunden, die Architektur und Entwerfen Sie Lösungen definieren.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Tom Laird-McConnell