Indizierung vorhandener Daten mit SolrJ in Apache Solr
Zwei beliebte Methoden zur Indizierung vorhandener Daten sind der Data Import Handler (DIH) und Tika (Solr Cell)/ExtractingRequestHandler. Diese können zur Indizierung von Daten aus einer Datenbank oder strukturierten Dokumenten (z.B. Word-Dokumente oder PDF oder ….) verwendet werden. Dies sind großartige Tools, um Dinge schnell zum Laufen zu bringen, und ich habe Produktionsseiten gesehen, die mit einem oder beiden dieser Tools gut funktionieren.
OK, warum dann über SolrJ sprechen?
Nun, irgendwo in dem architektonischen Dokument befinden sich zwei Kästchen mit Beschriftungen wie dieser, die durch einen Pfeil verbunden sind:
Oh, na gut. Selten wird die Verbindung zwischen dem Solr Server/Indexer und den Daten, die er indizieren soll, als „wunderbare Verbindung“ bezeichnet, aber ich wünschte mir manchmal, dass die Leute ehrlicher damit umgehen würden. Alle möglichen Dinge können sich hier in den Weg stellen, ich nenne nur 0,01% von ihnen:
- Die Sicherheitsbeauftragten werden NICHT „öffnen Sie bitte einfach die Datenbank für die IP-Adresse des Solr-Indexers“.
- Eigentlich ist es nicht nur eine Datenquelle. Es sind drei. Mindestens. Und übrigens: Sie müssen die Daten von Datenbank 1 unbedingt zwischenspeichern, sonst leidet die Leistung. Vergessen Sie nicht, dass jede dieser Datenbanken unterschiedliche Anmeldeinformationen benötigt.
- Habe ich schon erwähnt, dass die „wundersame Verbindung“ der Ort ist, an dem sich unsere Geschäftsregeln befinden, die in den Solr-Dokumenten kodiert werden müssen?
- Hey! Ich dachte, wir könnten die Dokumente bei diesem Schritt durch die Kategorisierung laufen lassen!
- Die Metadaten befinden sich in der DB, und Sie nehmen diese Informationen und fragen den Dateiserver ab, der Ihnen das Dokument liefert.
- Wir indizieren Filme. Richtig, Sie wollen nur die Metadaten. Aber was? Solr wird 99,99999% der Daten wegwerfen, nachdem Sie wie viele Daten über mein Netzwerk übertragen haben?
- <Fügen Sie hier Ihr Lieblingsproblem ein>
Und dabei ist noch nicht einmal erwähnt, dass DIH und Tika auf dem Server ausgeführt werden. Das heißt, der Solr-Indexer macht die ganze Arbeit und das arme Ding kann nur so schnell sein (na gut, er flitzt, aber das Parsen von einer Bazillion PDF/Word/Excel-Dokumenten ist eine ziemliche Belastung für einen einzelnen Rechner).
Ich will damit sagen, dass es oft gute Gründe gibt, warum der Einsatz von DIH und/oder Tika nicht optimal ist. Diese reichen von der Sicherheit über mehr Kontrolle darüber, wie „schlechte“ Dokumente behandelt werden, bis hin zum Durchsatz und der Frage, mit welchen Tools Ihr Unternehmen am besten zurechtkommt. Für diese Situationen ist SolrJ vielleicht am besten geeignet. Im Folgenden finden Sie ein Grundgerüst für das Programm:
- Stellt eine Verbindung von einem Java-Programm zu einem Solr-Server her, in diesem Fall dem Indexer.
- Fragt eine Datenbank über JDBC ab und wählt Informationen aus einer Tabelle aus und bringt sie in eine für die Indizierung geeignete Form.
- Durchläuft ein Dateisystem und indiziert alle Dokumente, die Tika in einem bestimmten Verzeichnis analysieren kann.
- Fügt diese Dokumente dem Solr-Server hinzu.
Bitte beachten Sie, dass das Beispiel sehr einfach ist. Nichts, was hier gemacht wird, könnte nicht auch mit DIH und Solr Cell gemacht werden. Die Absicht ist, Ihnen einen Ausgangspunkt zu bieten, den Sie an Ihre spezielle Situation anpassen können, wenn DIH und Solr Cell nicht sofort funktionieren.
Viel Aufbau, nicht viel Code
Für all das ist das Programm selbst ziemlich kurz. Ich werde einige Höhepunkte skizzieren, und die vollständige Liste finden Sie am Ende dieses Artikels.
Gläser und wo man sie bekommt.
Es gibt drei Sätze von jar-Dateien, die Sie benötigen, um dieses Beispiel auszuführen.
- Die Solr jar-Dateien. Es gibt zwei Orte, an denen Sie danach suchen können: <solr_home>/dist und <solr_home>/dist/solrj-lib. In diesen beiden Verzeichnissen befinden sich die Klassen, die Sie benötigen, damit eine SolrJ-Datei ihre Aufgaben erfüllen kann.
- Die Tika jar-Dateien. Ich empfehle Ihnen, Tika vom Apache-Projekt herunterzuladen (siehe Apache Tika ) und diese Jars in den Klassenpfad Ihres Clients aufzunehmen.
- Der entsprechende JDBC-Treiber. Dieser hängt von der Datenbank ab, zu der Sie eine Verbindung herstellen möchten. Oft ist er irgendwo in Ihrer Datenbankinstallation vorhanden, aber suchen Sie einfach nach „jdbc <Ihre Datenbank hier>“ und Sie sollten ihn finden können.
Beachten Sie, dass die einzige Änderung auf dem Server (und auch nur dann, wenn Sie das SolrJ-Programm auf dem Server ausführen) der JDBC-Treiber ist (falls erforderlich)! Wenn Sie den Server und den Client auf demselben Rechner ausführen, können Sie die entsprechenden Pfade einfach zu Ihrer Umgebungsvariablen CLASSPATH hinzufügen. Andernfalls müssen Sie alle benötigten Jars vom Server auf Ihren Client kopieren.
So sieht der Code aus. Der vollständige Quellcode am Ende kümmert sich um die Durchquerung des Dateisystems usw.
Einrichten der Solr-Verbindung
Dies ist nur der Code zum Einrichten der Verbindung mit dem Solr-Server. Die Verbindungszeichenfolge ist die zu ZooKeeper, da dieses Beispiel für eine SolrCloud-Installation ist. Dies sollte die gleiche Zeichenfolge sein, die Sie zum Starten von Solr verwenden. In diesem Beispiel verwende ich einen einzelnen Zookeeper, aber in Wirklichkeit wird es der „übliche“ Ensemble-String sein. Der einzige zusätzliche Teil ist am Ende, wo wir den Tika-Parser einrichten.
private SqlTikaExample(String url) throws IOException, SolrServerException { // Create a SolrCloud-aware client to send docs to Solr // Use something like HttpSolrClient for stand-alone client = new CloudSolrClient.Builder().withZkHost(zkEnsemble).build(); // The Solr 8x uses a builder pattern for creating a client.
// client = new CloudSolrClient.Builder(Collections.singletonList(zkEnsemble), Optional.empty()) // .withConnectionTimeout(5000) //.withSocketTimeout(10000) //.build();
// binary parser is used by default for responses client.setParser(new XMLResponseParser());
Ein strukturiertes Dokument mit Tika aus einem SolrJ-Programm indizieren
ContentHandler textHandler = new BodyContentHandler(); Metadata metadata = new Metadata(); ParseContext context = new ParseContext(); // Tim Allison noted the following, thanks Tim! // If you want Tika to parse embedded files (attachments within your .doc or any other embedded // files), you need to send in the autodetectparser in the parsecontext: // context.set(Parser.class, autoParser); InputStream input = new FileInputStream(file); // Try parsing the file. Note we haven't checked at all to // see whether this file is a good candidate. try { autoParser.parse(input, textHandler, metadata, context); } catch (Exception e) { // Needs better logging of what went wrong in order to // track down "bad" documents. log(String.format("File %s failed", file.getCanonicalPath())); e.printStackTrace(); continue; } // Just to show how much meta-data and what form it's in. dumpMetadata(file.getCanonicalPath(), metadata); // Index just a couple of the meta-data fields. SolrInputDocument doc = new SolrInputDocument(); doc.addField("id", file.getCanonicalPath());
Es gibt eine äußere Schleife, die für die Durchquerung des Dateisystems zuständig ist und die ich nicht gezeigt habe. Alles, was hier passiert, ist, dass Tika sein Ding machen darf. Wenn Tika das Dokument nicht parsen kann (beachten Sie, dass ich nicht darauf geachtet habe, dass die Dateien vernünftig sind, z.B. eine jar-Datei oder eine exe-Datei oder was auch immer geparst werden könnte), protokollieren wir einen Fehler und fahren fort.
Wenn das Dokument geparst wird, extrahieren wir ein paar Felder und geben sie in das Solr-Dokument ein. Dieses Dokument wird dann zu einer Java-Liste hinzugefügt, und wenn die Liste schließlich 1.000 Dokumente enthält, wird das Ganze zur Indizierung an Solr übergeben, wie Sie in der vollständigen Liste sehen können. Übrigens: Im Beispielindex, der mit der Solr-Distribution geliefert wird, sind diese Felder bereits definiert.
Aber beachten Sie hier eine Spitzfindigkeit, selbst im trivialen Fall. Wir nehmen an, dass das Metadatenfeld für den Autor „Autor“ heißt. Es gibt keine formatübergreifenden Standards für dieses Feld, es könnte „document_author“ heißen, oder vielleicht wollen Sie „last_editor“. Sie können all dies hier entweder durch eine vernünftige Konfiguration von Tika oder programmatisch steuern.
Weiter zum Sql-Bit
Als nächstes sehen wir uns den Code an, der über JDBC eine Verbindung zu einer MySql-Datenbank herstellt. Auch hier handelt es sich um die einfachste aller Datenbanktabellen und die einfachste aller Extraktionen. Sie werden die SolrJ-Lösung wahrscheinlich nicht verwenden, es sei denn, Ihre Situation ist komplexer als diese, aber das ist ein guter Anfang.
Class.forName("com.mysql.jdbc.Driver").newInstance(); log("Driver Loaded......"); con = DriverManager.getConnection("jdbc:mysql://192.168.1.103:3306/test?" + "user=testuser&password=test123"); Statement st = con.createStatement(); ResultSet rs = st.executeQuery("select id,title,text from test"); while (rs.next()) { // DO NOT move this outside the while loop SolrInputDocument doc = new SolrInputDocument(); String id = rs.getString("id"); String title = rs.getString("title"); String text = rs.getString("text"); doc.addField("id", id); doc.addField("title", title); doc.addField("text", text); docList.add(doc);
Auch hier handelt es sich um den gleichen Prozess wie beim letzten Mal, aber anstatt das strukturierte Dokument zu parsen, holen wir Zeilen aus einer Tabelle und fügen jedem Solr-Dokument ausgewählte Werte aus diesen Zeilen hinzu. Und wieder sammeln wir diese Dokumente in einer Liste, die schließlich an Solr gesendet wird.
Fazit
Wie Sie sehen können, ist die Verwendung von Tika und/oder SQL/JDBC von einem SolrJ-Client aus nicht sehr kompliziert. Ich nehme an, dass dieser Blog durch die vielen Anfragen auf der Solr-Benutzerliste veranlasst wurde, die nach Beispielen für die Verwendung von SolrJ zur Indizierung von Dokumenten fragen. Es ist ziemlich entmutigend, mit der gesamten Solr API-Dokumentation konfrontiert zu werden und keine Ahnung zu haben, wo man anfangen soll. Ich hoffe, dieses Beispiel entmystifiziert den Prozess ein wenig.
Umwelt
Ich habe diesen Code im März 2017 gegen Solr 6.x kompiliert, da der Code ziemlich alt war und einige der Klassen nicht mehr verfügbar waren. Allerdings habe ich keine vollständige Testumgebung mehr eingerichtet, da sich die Änderungen ausschließlich auf CloudSolrClient bezogen.
Vollständiger Quellcode und Haftungsausschluss
Einer der Vorzüge beim Schreiben von Beispielen ist, dass man die ganze hässliche Fehlerbehandlung, Protokollierung usw. weglassen kann. Dieser Code muss für Produktionszwecke erheblich verbessert werden, und Ihre Situation wird mit Sicherheit viel komplexer sein, sonst müssten Sie sich gar nicht erst mit SolrJ beschäftigen! Sie können dies also gerne als Grundlage für die weitere Arbeit mit SolrJ verwenden, aber es ist schließlich nur ein Beispiel.
Beachten Sie auch, dass ich SolrJ als Beispiel verwendet habe, aber es gibt auch andere Implementierungen, C#, PHP usw. Soweit ich weiß, verfügen andere Clients als SolrJ jedoch nicht über das in CloudSolrClient integrierte Routing, so dass sie in einer SolrCloud-Umgebung deutlich weniger effizient sind als SolrJ.
package solrjexample; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.impl.XMLResponseParser; import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.common.SolrInputDocument; import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; import org.apache.tika.parser.ParseContext; import org.apache.tika.sax.BodyContentHandler; import org.xml.sax.ContentHandler; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.sql.*; import java.util.ArrayList; import java.util.Collection; /* Example class showing the skeleton of using Tika and Sql on the client to index documents from both structured documents and a SQL database. NOTE: The SQL example and the Tika example are entirely orthogonal. Both are included here to make a more interesting example, but you can omit either of them. */ public class SqlTikaExample { private CloudSolrClient client; private long start = System.currentTimeMillis(); private AutoDetectParser autoParser; private int totalTika = 0; private int totalSql = 0; private final String zkEnsemble = "http://localhost:2181"; private Collection docList = new ArrayList(); public static void main(String[] args) { try { SqlTikaExample idxer = new SqlTikaExample("http://localhost:8983/solr"); idxer.doTikaDocuments(new File("/Users/Erick/testdocs")); idxer.doSqlDocuments(); idxer.endIndexing(); } catch (Exception e) { e.printStackTrace(); } } private SqlTikaExample(String url) throws IOException, SolrServerException { // Create a SolrCloud-aware client to send docs to Solr // Use something like HttpSolrClient for stand-alone client = new CloudSolrClient.Builder().withZkHost(zkEnsemble).build(); // Solr 8 uses a builder pattern here. // client = new CloudSolrClient.Builder(Collections.singletonList(zkEnsemble), Optional.empty()) // .withConnectionTimeout(5000) // .withSocketTimeout(10000) // .build(); // binary parser is used by default for responses client.setParser(new XMLResponseParser()); // One of the ways Tika can be used to attempt to parse arbitrary files. autoParser = new AutoDetectParser(); } // Just a convenient place to wrap things up. private void endIndexing() throws IOException, SolrServerException { if ( docList.size() > 0) { // Are there any documents left over? client.add(docList, 300000); // Commit within 5 minutes } client.commit(); // Only needs to be done at the end, // commitWithin should do the rest. // Could even be omitted // assuming commitWithin was specified. long endTime = System.currentTimeMillis(); log("Total Time Taken: " + (endTime - start) + " milliseconds to index " + totalSql + " SQL rows and " + totalTika + " documents"); } // I hate writing System.out.println() everyplace, // besides this gives a central place to convert to true logging // in a production system. private static void log(String msg) { System.out.println(msg); } /** * ***************************Tika processing here */ // Recursively traverse the filesystem, parsing everything found. private void doTikaDocuments(File root) throws IOException, SolrServerException { // Simple loop for recursively indexing all the files // in the root directory passed in. for (File file : root.listFiles()) { if (file.isDirectory()) { doTikaDocuments(file); continue; } // Get ready to parse the file. ContentHandler textHandler = new BodyContentHandler(); Metadata metadata = new Metadata(); ParseContext context = new ParseContext(); // Tim Allison noted the following, thanks Tim! // If you want Tika to parse embedded files (attachments within your .doc or any other embedded // files), you need to send in the autodetectparser in the parsecontext: // context.set(Parser.class, autoParser); InputStream input = new FileInputStream(file); // Try parsing the file. Note we haven't checked at all to // see whether this file is a good candidate. try { autoParser.parse(input, textHandler, metadata, context); } catch (Exception e) { // Needs better logging of what went wrong in order to // track down "bad" documents. log(String.format("File %s failed", file.getCanonicalPath())); e.printStackTrace(); continue; } // Just to show how much meta-data and what form it's in. dumpMetadata(file.getCanonicalPath(), metadata); // Index just a couple of the meta-data fields. SolrInputDocument doc = new SolrInputDocument(); doc.addField("id", file.getCanonicalPath()); // Crude way to get known meta-data fields. // Also possible to write a simple loop to examine all the // metadata returned and selectively index it and/or // just get a list of them. // One can also use the Lucidworks field mapping to // accomplish much the same thing. String author = metadata.get("Author"); if (author != null) { doc.addField("author", author); } doc.addField("text", textHandler.toString()); docList.add(doc); ++totalTika; // Completely arbitrary, just batch up more than one document // for throughput! if ( docList.size() >= 1000) { // Commit within 5 minutes. UpdateResponse resp = client.add(docList, 300000); if (resp.getStatus() != 0) { log("Some horrible error has occurred, status is: " + resp.getStatus()); } docList.clear(); } } } // Just to show all the metadata that's available. private void dumpMetadata(String fileName, Metadata metadata) { log("Dumping metadata for file: " + fileName); for (String name : metadata.names()) { log(name + ":" + metadata.get(name)); } log("nn"); } /** * ***************************SQL processing here */ private void doSqlDocuments() throws SQLException { Connection con = null; try { Class.forName("com.mysql.jdbc.Driver").newInstance(); log("Driver Loaded......"); con = DriverManager.getConnection("jdbc:mysql://192.168.1.103:3306/test?" + "user=testuser&password=test123"); Statement st = con.createStatement(); ResultSet rs = st.executeQuery("select id,title,text from test"); while (rs.next()) { // DO NOT move this outside the while loop SolrInputDocument doc = new SolrInputDocument(); String id = rs.getString("id"); String title = rs.getString("title"); String text = rs.getString("text"); doc.addField("id", id); doc.addField("title", title); doc.addField("text", text); docList.add(doc); ++totalSql; // Completely arbitrary, just batch up more than one // document for throughput! if ( docList.size() > 1000) { // Commit within 5 minutes. UpdateResponse resp = client.add(docList, 300000); if (resp.getStatus() != 0) { log("Some horrible error has occurred, status is: " + resp.getStatus()); } docList.clear(); } } } catch (Exception ex) { ex.printStackTrace(); } finally { if (con != null) { con.close(); } } } }
Dieser Beitrag wurde ursprünglich am 14. Februar 2012 veröffentlicht.