12. April 2013 · von Steffen Nörtershäuser

Erklärt: Document Search

In dem vorherigen Artikel Sales Content Management haben wir bereits von unserer Document Search Lösung berichtet. Diese ermöglicht es Dynamics CRM Nutzern, über die SharePoint Volltextsuche Dokumente in CRM zu durchsuchen. In diesem Blog-Artikel werde ich auf die technischen Details hinter dieser Lösung eingehen.

Das Ziel der Dokumenten Suche ist es dem Nutzer strukturiert und nach den CRM Entitäten gruppiert die Ergebnisse der SharePoint Volltext Suche aufzuzeigen. Zur Verdeutlichung habe ich einen Screenshot angehangen:

CRM Document Search

Technische Einschränkungen:
Bei der Entwicklung der Document Search gab es einige technische Einschränkungen die berücksichtigt werden mussten. So war es nicht möglich die Document Search Seite als eine ASP Seite zu implementieren und die Webservice aufrufe in der Code-Behind Klasse umzusetzen. Hätte man die Seite als ASP Seite umgesetzt, wäre es nicht länger möglich gewesen alle notwendigen Daten als CRM-Lösung zu kapseln. Man hätte in diesem Fall mehrere Dateien in den ISV Ordner von CRM bereitstellen müssen. Daher musste die Funktionalität als HTML-Seite mit Javascript umgesetzt werden.
Desweiteren war es nicht möglich das Standard-Grid von CRM zur Darstellung der Daten zu nutzen, da dies nicht die Möglichkeit einer Gruppierung wie im Screenshot dargestellt bietet. Auch kann man keine eigenen Spalten dort einfügen sondern lediglich das unterliegende Fetch-XML ändern.

Voraussetzungen
Um die Entwicklung mit Javascript zu vereinfachen und produktiv zu gestalten wird die Javascript Bibliotheken jQuery, XrmServiceToolkit sowie jQuery SharePoint Web Services genutzt. Das XrmServiceToolkit muss dabei geringfügig angepasst werden damit die Server Url korrekt abgefragt werden kann. Weil die Document Search Seite durch CRM in ein iframe eingebettet wird, kann das Xrm Objekt, welches die CRM Server Url beinhaltet, nur über das window.parent Objekt abgefragt werden.
Um das CRM Grid zu erzeugen wird ein einfacher HTML-Table erzeugt mit entsprechenden CSS-Styles. Über Javascript wird die Möglichkeit geboten die Größe der Spalten und die Sortierung anzupassen. Ich möchte im Rahmen dieses Artikels jedoch nicht näher auf das Grid eingehen da dies den Rahmen eines eigenen Artikels einnehmen würde.

Grober Ablauf der Suche:
Der Ablauf der Suche gliedert sich wie folgt:

  1. Intialisierung
    Hier werden die Konfigurationsdaten etc. geladen.
  2. Suchanfrage an SharePoint
    In diesem Schritt wird die Suchanfrage an die SharePoint Webservices gestellt.
  3. Gruppieren der Suchergebnisse
    An dieser Stelle werden die gefundenen Dokumente nach Pfaden gruppiert.
  4. Abfragen der CRM Speicherorte
    Um die Gruppierung im Grid aufzubauen werden hier die Speicherorte innerhalb von CRM und die zugehörigen Entitäten abgefragt.

Den Ablauf der Webservice Aufrufe habe ich im Folgenden grafisch dargestellt, um klar zu machen wann auf welchen Server zugegriffen wird:
DocumentSearch

Initialisierung:
Die SharePoint Seiten, in denen CRM Dokumente ablegt, werden in der Entität „sharepointsite“ gespeichert. Diese werden im ersten Schritt abgefragt, um anschließend bei der Suchanfrage die korrekte SharePoint URL abfragen zu können, ohne dass der Nutzer an einer weiteren Stelle die Dokumenten Suche konfigurieren muss. Mit Hilfe des XrmServiceToolkit können diese Daten sehr einfach abgefragt werden. Aus Einfachheitsgründen wird hier nur eine SharePoint Site unterstützt. Hier ein kleines Code-Snippet welches dies durchführt:

//FetchXML aufbauen - hier werden alle SharePoint Sites und ihrer Absolute URL abgefragt
var fetchXml =
	"<fetch mapping='logical' version='1.0'>" +
	    "<entity name='sharepointsite'>" +
		"<attribute name='sharepointsiteid'/>" +
		"<attribute name='absoluteurl'/>" +
	    "</entity>" +
	"</fetch>";

//WebServices aufrufen starten
var retrievedSharePointSites = XrmServiceToolkit.Soap.Fetch(fetchXml);

//Prüfen ob korrekt konfigurierte SharePoint Site vorhanden ist
if (retrievedSharePointSites.length == 0) {
    alert("Keine SharePoint Site konfiguriert.");
    return false;
}

if (retrievedSharePointSites[0].attributes["absoluteurl"] == null) {
    alert("Fehlerhaft konfigurierte SharePoint Site gefunden.");
    return false;
}

//Site URL speichern
WEB_URL = retrievedSharePointSites[0].attributes["absoluteurl"].value;

Zusätzlich werden in diesem Schritt Icons der Entitäten abgefragt. Dafür wird ein Metadaten Request abgesendet. Der Request sieht wie folgt aus:

requestMain += "      <request i:type="a:RetrieveAllEntitiesRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">";
requestMain += "        <a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">";
requestMain += "          <a:KeyValuePairOfstringanyType>";
requestMain += "            <b:key>EntityFilters</b:key>";
requestMain += "            <b:value i:type="c:EntityFilters" xmlns:c="http://schemas.microsoft.com/xrm/2011/Metadata">Privileges</b:value>";
requestMain += "          </a:KeyValuePairOfstringanyType>";
requestMain += "          <a:KeyValuePairOfstringanyType>";
requestMain += "            <b:key>RetrieveAsIfPublished</b:key>";
requestMain += "            <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">true</b:value>";
requestMain += "          </a:KeyValuePairOfstringanyType>";
requestMain += "        </a:Parameters>";
requestMain += "        <a:RequestId i:nil="true" />";
requestMain += "        <a:RequestName>RetrieveAllEntities</a:RequestName>";
requestMain += "      </request>";

Dieser kann anschließend über das XrmServiceToolkit an CRM abgesetzt werden. Hierfür stellt das Toolkit die XrmServiceToolkit.Soap.Execute(request) Methode bereit. Diese liefert ein Xml-Dokument zurück mit den notwendigen Daten. Die „c:EntityMetadata“ Tags enthalten jeweils die Metadaten für eine Entität. Folgendes Code-Snippet verdeutlicht den Ablauf, die Icons aus diesem Xml-Dokument zu parsen:

//Metadaten Knoten suchen
metaData = xmlDoc.getElementsByTagName("c:EntityMetadata");
for (curEntity = 0; curEntity < metaData.length; curEntity++) {
    //Entitätsdaten parsen
    var entityNode = metaData[curEntity];
    var logicalName = entityNode.getElementsByTagName("c:LogicalName")[0].text;
    var icon = entityNode.getElementsByTagName("c:IconSmallName")[0].text;
    if (icon == "" || icon == null) {
	//Wenn kein Icon definiert ist kann das Icon über den Entitäts Type Code geparst werden
	if (entityNode.getElementsByTagName("c:IsCustomEntity")[0].text != "true") {
	    icon = "/_imgs/ico_16_" + entityNode.getElementsByTagName("c:ObjectTypeCode")[0].text + ".gif?ver=-1672284805";
	}
	else {
	    icon = "/_Common/icon.aspx?cache=1&iconType=GridIcon&objectTypeCode=" + entityNode.getElementsByTagName("c:ObjectTypeCode")[0].text;
	}
    }

    //Icon speichern
    ENTITY_ICONS[logicalName] = icon;

    //Obj Code speichern
    ENTITY_OBJ_CODES[logicalName] = entityNode.getElementsByTagName("c:ObjectTypeCode")[0].text;
}

Nach diesen Schritten sind die notwendigen Daten gespeichert und die Suche kann durchgeführt werden.

Suchanfrage an SharePoint
Nachdem der Nutzer einen Suchtext eingegeben hat und den Such Button betätigt hat wird eine Webservice Anfrage an den SharePoint Server gesendet, um die möglichen Treffer der Suche zu ermitteln. Mögliche Treffer, weil nicht jedes Dokument, welches Beispielsweise das Wort „Sample“ beinhaltet, auch in CRM einer Entität zugeordnet ist. Diese Dokumente werden im nächsten Schritt herausgefiltert.
Die Anfrage an den SharePoint Server ist Dank der jQuery SharePoint Web Services sehr einfach durchzuführen:

var queryXml = "<QueryPacket xmlns='urn:Microsoft.Search.Query'>" +
			"<Query>" + 
				"<SupportedFormats>" + 
					"<Format revision='1'>" +
						"urn:Microsoft.Search.Response.Document:Document" +
					"</Format>" +
				"</SupportedFormats>" +
				"<TrimDuplicates>false</TrimDuplicates>" +
				"<Range>" + 
					"<StartAt>1</StartAt>" +
					"<Count>1000</Count>" + 
				"</Range>" +
				"<Context>" +
					"<QueryText language='en-US' type='STRING'>" + searchString + "</QueryText>" +
				"</Context>" +
			"</Query>" +
		       "</QueryPacket>";

$().SPServices({
    operation: "QueryEx",
    webURL: WEB_URL,	//Dies ist die WEB_URL die in der Init Phase abgefragt wurde
    queryXml: queryXml,
    completefunc: function (xData, Status) {
       //Daten Verarbeiten und CRM Werte abfragen, wird im nächsten Schritt erklärt
    }
});

Der interessante Teil des obigen Auszug ist der Aufbau der CAML-Query für SharePoint. Hervor zuheben ist hier insbesondere der Punkt „TrimDuplicates“. Wird dieser Knoten nicht angegebene und auf false gesetzt, kürzt SharePoint Duplikate aus den Suchergebnisse. Das bedeutet das SharePoint identische Dokumente an verschiedenen Speicherorten nur einmal in der Suche zurückgibt und nur vermerkt, dass weitere Duplikate vorhanden sind. Hängt ein CRM Nutzer nun beispielsweise eine Mitteilung mehreren Firmen etc. an, würde dieses Dokument nur einmal im Suchergebnis auftauchen. Des Weiteren muss hier eine Range angegeben werden. Ansonsten liefert der SharePoint nur eine begrenzte Anzahl an Ergebnissen und der Nutzer der Dokumenten Suche würde erneut nicht alle Dokumente finden.

In der Funktion, welche unter „completefunc“ definiert ist, werden nun die Dokumente nach Entitäten gruppiert und nicht zugeordnete Dokumente herausgefiltert.

Gruppieren der Suchergebnisse
CRM legt für jede Entität, welche Dokumente im SharePoint speichert, einen Ordner im SharePoint an. Dokumente, welche im gleichen Pfad liegen, gehören somit zu ein und derselben Entität in CRM. Aus diesem Grund werden die Suchergebnisse hier nach Pfad gruppiert. Über jQuery kann das XML Dokument, welches von den SharePoint Webservices zurückgeliefert wird, einfach geparst werden. Das folgende Code-Snippet zeigt, wie diese Gruppierung abläuft:

searchResultsGroupByPath = new Object();

$(xData.responseText).find("RelevantResults").each(function () {
    var filenameAndPath = $(this).find("Path").text();
    var filename = filenameAndPath.split('/').pop();
    var path = filenameAndPath.substr(0, filenameAndPath.lastIndexOf('/'));

    //Ergebnis in Liste hinzufügen und nach Pfad gruppieren
    if (typeof searchResultsGroupByPath[path] == "undefined") {
	searchResultsGroupByPath[path] = new Array();
    }

    var resultObject = new Object();
    resultObject.Filename = filename;
    resultObject.FilenameAndPath = filenameAndPath;
    resultObject.Preview = replaceSearchResultTags($(this).find("HitHighlightedSummary").html());
    resultObject.Title = $(this).find("Title").text();

    searchResultsGroupByPath[path][searchResultsGroupByPath[path].length] = resultObject;
});

Wie hier zu sehen ist wird ein Javascript Objekt als Assoziatives Array genutzt, um nach Pfaden zu gruppieren. Zu jedem Pfad werden die Suchergebnisse mit Dateinamen, Vorschau und Titel gespeichert.

An dieser Stelle liegen die Suchergebnisse jetzt in aufbereiteter und nach Pfaden gruppierten Form vor und die zugehörigen CRM-Daten können abgefragt werden.

Abfragen der CRM Speicherorte
Bevor ich näher auf die Webservices Aufrufe eingehe, möchte ich aufzeigen, wie CRM zugeordnete Dokumente abspeichert. Wichtig dabei ist zu wissen dass CRM nicht einzelne Dokumente speichert, sondern lediglich Dokumente Speicherorte. Diese entsprechen einem Ordner in SharePoint. Zusätzlich werden diese Speicherorte hierarchisch abgespeichert, d.h. ein Speicherort besitzt nicht die Absolute URL zum SharePoint Server sondern nur die relative zum übergeordneten Speicherort (auch wenn ein Feld dort existiert welches „absoluteurl“ heißt, ist dies nicht immer ausgefüllt und somit nicht verlässlich nutzbar).
Dies möchte ich mit einem kleinen Beispiel näher beleuchten: Angenommen, es existiert eine Firma „Fast Track AG“ mit mehreren zugeordneten Dokumenten, dann existiert auf dem SharePoint Server eine Dokumenten Bibliothek „Account“ mit einem Ordner „Fast Track AG“. In diesem Ordner sind alle zugeordneten Dokumente gespeichert. Die dazu im CRM vorhandenen Daten sähen wie folgt aus:

Dokumentenspeicherort „Account“:
Verweist auf die SharePoint Dokumenten Bibliothek „Account“. Hat als Relative Url gespeichert „account“.

Untergeordnet und über „parentsiteorlocation“ Attribute mit „Account“ Speicherort verbunden:
Dokumentenspeicherort „Fast Track AG“:
Verweist auf den SharePoint Ordner „Fast Track AG“.
Hat als Relative Url gespeichert „fast%20track%20ag“.
Beinhaltet Verweis auf Firma „Fast Track AG“

Wie die Daten zu einem Suchergebnisse ermittelt werden können zeigt dieses Code-Snippet:

//Pfad säubern und trennen
var splittedPath = curPath.replace(WEB_URL, "").split("/");

//Zugehörige Entität ermitteln
var oldLocation = null;
var isValid = true;
for (curSplitPath = 0; curSplitPath < splittedPath.length; ++curSplitPath) {
	var documentLocation = getDocumentLocation(splittedPath[curSplitPath], oldLocation);

	//Prüfen ob es sich um einen gültigen Pfad handelt
	if (documentLocation == null && oldLocation == null) {
	    isValid = false;
	    break;
	}

	oldLocation = documentLocation;
}

if (!isValid) {
	continue;
}

//Prüfen ob Entität zugeordnet ist
if (oldLocation == null || oldLocation.attributes["regardingobjectid"] == null) { 
	continue;
}

//Entitätsdaten anlegen
var entityData = new Object();
entityData.logicalName = oldLocation.attributes["regardingobjectid"].logicalName;
entityData.id = oldLocation.attributes["regardingobjectid"].id;
entityData.name = oldLocation.attributes["regardingobjectid"].name;
entityData.Path = curPath;

/** brief Ermittelt den SharePoint Dokumenten Speicherort zu einem Teilpfad
   * param splittedPath Teilpfad der gesucht wird
   * param oldLocation Alter SharePoint Dokumenten Speicherort des vorherigen Teilpfad
   * return SharePoint Dokumenten Speicherort, null wenn keiner gefunden wurde
   */
function getDocumentLocation(splittedPath, oldLocation) {
	//Dokumenten Location ermitteln
	var fetchXml =
		"<fetch mapping='logical' version='1.0'>" +
		    "<entity name='sharepointdocumentlocation'>" +
			"<attribute name='sharepointdocumentlocationid'/>" +
			"<attribute name='regardingobjectid'/>" +
			"<attribute name='regardingobjecttypecode'/>" +
			"<filter type='and'>" +
			    "<condition attribute='relativeurl' operator='eq' value='" + splittedPath + "' />";

	if (oldLocation != null && typeof oldLocation != "undefined") {
	    fetchXml +=     "<condition attribute='parentsiteorlocation' operator='eq' value='" + oldLocation.id + "' />";
	}

	fetchXml += "</filter>" +
		"</entity>" +
	    "</fetch>";

	//Dokumenten Location abfragen
	var retrievedDocumentLocation = XrmServiceToolkit.Soap.Fetch(fetchXml);

	if (retrievedDocumentLocation.length == 0) {
	    return null;
	}

	return retrievedDocumentLocation[0];
}

Auf diese Weise muss zu jedem gespeicherten Pfad die zugehörigen Entitätsdaten abgefragt werden. Sind keine Entitätsdaten zugeordnet oder ist der Pfad überhaupt nicht in CRM zu finden, so handelt es sich um ein Dokument, welches nicht mit CRM verknüpft ist und es kann ignoriert werden.

Nun liegen alle notwendigen Daten vor um die Ergebnisse in das Custom Grid einzufügen. Jede gefundene Entität wird als eine übergeordnete Zeile (im Screenshot zu Beginn beispielsweise die Fast Track AG oder Max Master) eingefügt. Anschließend müssen alle Dokumente, die im Pfad der Entität gefunden wurden, als Dokumentenzeile einfügt werden (Sample Document, Test.pdf…).

Fazit
Die Document Search ermöglicht es Nutzern auf einfache Art und Weise, den Dokumenten Bestand von CRM zu durchsuchen und die zugehörigen Entitäten anzuzeigen. Diese Funktionalität bietet CRM so standardmäßig nicht an.

Eine einfache Implementierung hierzu lässt sich auf Codeplex unter CRM 2011 Document Search finden.

 



Diesen Blogeintrag bewerten:

2 Stimmen mit durchschnittlich 2/5 Punkten

Haben Sie Fragen zu diesem Artikel oder brauchen Sie Unterstützung?

Nehmen Sie mit uns Kontakt auf!

Wir unterstützen Sie gerne bei Ihren SharePoint-Vorhaben!


Schreibe einen Kommentar

Kontakt.
Lassen Sie sich von uns beraten
Wir freuen uns über Ihr Interesse an unseren Leistungen. Hinterlassen Sie
uns Ihren Namen, Ihre Telefonnummer und E-Mail Adresse – wir melden
uns schnellstmöglich bei Ihnen.
Kontakt aufnehmen