From 4c3da7d842592ff2ffddb5c510dfd560f97e084a Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Wed, 15 Jul 2026 23:51:56 +0900 Subject: [PATCH] docs(dev): correct 15.8 data store plugin guide against implementation Verify the developer guide for building a data store plugin against the current source code and fix inaccuracies, then re-translate every language version from the corrected Japanese source. - Fix storeData() signature: paramMap is DataStoreParams, not Map; add the DataStoreParams import - Use paramMap.getAsString() instead of get() to read string parameters - Correct the field-mapping pipeline to match real connectors: build resultMap from paramMap.asMap() plus the source record, evaluate each scriptMap entry with convertValue(scriptType, template, resultMap), and call callback.store(paramMap, dataMap) - Use the correct DataConfig package (org.codelibs.fess.opensearch.config.exentity) - Show getName() as a protected method returning getClass().getSimpleName() - Add component registration via fess_ds++.xml (postConstruct register) - Add AbstractDataStore helper methods, a data-processing-flow section, and a build/installation section (pom.xml, mvn clean package, install) - Update the unit test example to JUnit 5 with UTFlute LastaDiTestCase - Re-translate en, de, es, fr, ko, and zh-cn (with correct diacritics) --- de/15.8/dev/datastore-plugin.rst | 482 +++++++++++++++++++------ en/15.8/dev/datastore-plugin.rst | 471 +++++++++++++++++++----- es/15.8/dev/datastore-plugin.rst | 540 +++++++++++++++++++++------- fr/15.8/dev/datastore-plugin.rst | 500 ++++++++++++++++++++------ ja/15.8/dev/datastore-plugin.rst | 422 +++++++++++++++++----- ko/15.8/dev/datastore-plugin.rst | 422 +++++++++++++++++----- zh-cn/15.8/dev/datastore-plugin.rst | 439 ++++++++++++++++------ 7 files changed, 2558 insertions(+), 718 deletions(-) diff --git a/de/15.8/dev/datastore-plugin.rst b/de/15.8/dev/datastore-plugin.rst index ddfb96be3..965aed6b6 100644 --- a/de/15.8/dev/datastore-plugin.rst +++ b/de/15.8/dev/datastore-plugin.rst @@ -5,16 +5,30 @@ DataStore-Plugin-Entwicklung Übersicht ========= -Durch die Entwicklung eines DataStore-Plugins konnen Sie |Fess| die Fahigkeit -zur Inhaltserfassung von neuen Datenquellen hinzufugen. +Durch die Entwicklung eines DataStore-Plugins können Sie |Fess| um die Fähigkeit +zur Inhaltserfassung aus neuen Datenquellen erweitern. Ein DataStore ruft +Datensätze aus externen Systemen wie Datenbanken, APIs oder Dateien ab, wandelt +sie gemäß dem in der Administrationsoberfläche konfigurierten Mapping-Skript in +Indexfelder um und registriert sie anschließend im Index von |Fess|. + +Alle öffentlich verfügbaren Konnektoren (``fess-ds-*``) für CSV, JSON, +Datenbanken, Git und verschiedene Cloud-Dienste sind nach diesem Mechanismus +implementiert. Als Implementierungsvorlage steht +`fess-ds-example `__ zur Verfügung. +Beim Erstellen eines neuen Konnektors ist es daher am einfachsten, dieses +Repository zu kopieren und darauf aufzubauen. Grundstruktur ============= -Ein DataStore-Plugin wird durch Vererbung von ``AbstractDataStore`` implementiert. +Ein DataStore-Plugin besteht aus den folgenden drei Elementen: + +- Erstellen einer Klasse, die von ``AbstractDataStore`` erbt +- Implementieren der beiden Methoden ``getName()`` und ``storeData()`` +- Registrieren als Komponente in ``fess_ds++.xml`` Minimale Implementierung ------------------------- +------------------------- .. code-block:: java @@ -24,170 +38,283 @@ Minimale Implementierung import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // Wird als Handler-Name verwendet. Es ist üblich, den einfachen + // Klassennamen zurückzugeben + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { // Datenabruf und -verarbeitung hier implementieren } } +.. note:: + + ``getName()`` und ``storeData()`` sind beide ``protected`` deklarierte + abstrakte Methoden. Beachten Sie, dass sich das Paket von ``DataConfig`` in + |Fess| 15.x unter ``org.codelibs.fess.opensearch.config.exentity`` befindet + (das frühere ``org.codelibs.fess.es.config.exentity`` wurde entfernt). + +Komponenten-Registrierung +-------------------------- + +Damit |Fess| den erstellten DataStore erkennt, wird die Komponente in +``src/main/resources/fess_ds++.xml`` registriert. + +.. code-block:: xml + + + + + + + + + +Durch ```` wird nach der Erzeugung der +Komponente automatisch die von ``AbstractDataStore`` bereitgestellte Methode +``register()`` aufgerufen, wodurch sich die Komponente selbst bei +``DataStoreFactory`` registriert. Der dabei registrierte Name ist der +Rückgabewert von ``getName()`` (im obigen Beispiel ``ExampleDataStore``) und +entspricht dem „Handler-Name", der in der Datenspeicher-Konfiguration der +Administrationsoberfläche ausgewählt wird. + AbstractDataStore ================= Wichtige Methoden ------------------ +------------------ .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 * - Methode + - Kategorie - Beschreibung * - ``getName()`` - - Gibt den Namen des DataStore zuruck (erforderlich) + - Implementierung (erforderlich) + - Abstrakte Methode, die den Handler-Namen des DataStore zurückgibt. Es ist üblich, ``getClass().getSimpleName()`` zurückzugeben * - ``storeData()`` - - Fuhrt Datenabruf und Index-Registrierung durch (erforderlich) + - Implementierung (erforderlich) + - Abstrakte Methode, die Datenabruf, -umwandlung und Index-Registrierung durchführt * - ``register()`` - - Registriert das Plugin + - Geerbt (in der Regel keine Änderung nötig) + - Wird über ``postConstruct`` in ``fess_ds++.xml`` automatisch aufgerufen und registriert die Komponente bei ``DataStoreFactory`` + * - ``store()`` + - Geerbt (Framework-Aufruf) + - Einstiegspunkt, der vom Framework aufgerufen wird. Bereitet u. a. ``defaultDataMap`` vor und ruft ``storeData()`` auf + * - ``convertValue()`` + - Geerbt (Hilfsmethode) + - Wertet den Wert (das Template) aus ``scriptMap`` mit der Skript-Engine aus + * - ``getScriptType()`` + - Geerbt (Hilfsmethode) + - Ruft den Parameter ``script_type`` ab (Standard ist Groovy) + * - ``getReadInterval()`` + - Geerbt (Hilfsmethode) + - Ruft den Parameter ``readInterval`` (in Millisekunden) ab + * - ``sleep()`` + - Geerbt (Hilfsmethode) + - Wartet die angegebene Anzahl an Millisekunden (wird zum Warten zwischen Datensätzen verwendet) + +Parameter von storeData +------------------------- -Parameter ---------- +An die Methode ``storeData()`` übergebene Parameter: -Parameter, die an die ``storeData()``-Methode ubergeben werden: +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 -- ``dataConfig``: DataStore-Konfiguration -- ``callback``: Callback fur Index-Updates -- ``paramMap``: In der Administrationsoberflache konfigurierte Parameter -- ``scriptMap``: Skript-Konfiguration -- ``defaultDataMap``: Standard-Daten-Map + * - Parameter + - Typ + - Beschreibung + * - ``dataConfig`` + - ``DataConfig`` + - Datenspeicher-Konfiguration (ID, Handler-Name, Parameter, Skript usw.) + * - ``callback`` + - ``IndexUpdateCallback`` + - Callback zur Registrierung des erzeugten Dokuments im Index + * - ``paramMap`` + - ``DataStoreParams`` + - Konfigurationswerte aus dem Feld „Parameter" der Administrationsoberfläche. Zugriff über ``getAsString(key)`` / ``getAsString(key, default)`` / ``get(key)`` / ``asMap()`` / ``containsKey(key)`` + * - ``scriptMap`` + - ``Map`` + - Konfiguration aus dem Feld „Skript" der Administrationsoberfläche. Der Schlüssel ist der Indexfeldname, der Wert das auszuwertende Skript-Template + * - ``defaultDataMap`` + - ``Map`` + - Standardfeldwerte jedes Dokuments (Konfigurations-ID, Boost, Rolle, MIME-Type, virtueller Host usw.), die vom Framework bereitgestellt werden + +.. warning:: + + Der Typ von ``paramMap`` ist nicht ``Map``, sondern + ``DataStoreParams``. Da ``DataStoreParams`` das Interface ``Map`` nicht + implementiert, verwenden Sie zum Abrufen von Werten nicht ``get()``, sondern + ``getAsString()``, das einen String zurückgibt. + +Ablauf der Datenverarbeitung +------------------------------ + +Die Implementierung von ``storeData()`` verarbeitet Daten in folgendem Ablauf. + +#. Abrufen der Quelldatensätze aus dem externen System. +#. Erstellen von ``resultMap`` durch Zusammenführen der Felder des + Quelldatensatzes mit ``paramMap.asMap()`` (das Skript wird gegen dieses + ``resultMap`` ausgewertet). +#. Auswerten jedes Eintrags von ``scriptMap`` mit + ``convertValue(scriptType, template, resultMap)`` und Speichern des + Ergebnisses in ``dataMap``. Wichtig ist, dass das Mapping nicht im Code fest + verdrahtet ist, sondern vom Administrator im Feld „Skript" definiert wird. +#. Aufrufen von ``callback.store(paramMap, dataMap)``, um das Dokument im Index + zu registrieren. Implementierungsbeispiel -======================== +========================== Einfacher DataStore -------------------- +--------------------- + +Ein Beispiel, das Datensätze von einer externen API abruft und im Index +registriert. .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // Parameter abrufen - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // Parameter abrufen (bei DataStoreParams wird getAsString verwendet) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // Auswertungstyp des Skripts (Standard ist Groovy) + final String scriptType = getScriptType(paramMap); + // Wartezeit zwischen Datensätzen (in Millisekunden) + final long readInterval = getReadInterval(paramMap); try { - // Daten abrufen - List documents = fetchDocuments(apiUrl, apiKey); + // Daten vom externen System abrufen + final List> records = fetchRecords(apiUrl, apiKey); - // Jedes Dokument verarbeiten - for (Document doc : documents) { + for (final Map record : records) { + // dataMap durch Kopieren der Standardfelder initialisieren final Map dataMap = new HashMap<>(defaultDataMap); - // Daten-Mapping - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // resultMap durch Zusammenführen von Parametern und + // Quelldatensatz erstellen + // (das Skript wird gegen dieses resultMap ausgewertet) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // Skript ausfuhren (Mapping) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // Jeden Eintrag von scriptMap auswerten, um Indexfelder zu erzeugen + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } // Im Index registrieren - callback.store(paramMap, resultMap); + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } -Mit Paginierung ---------------- +``fetchRecords()`` ist eine eigene Methode, die die Liste der Datensätze vom +externen System abruft. Die Feldnamen der einzelnen abgerufenen Datensätze +(``Map``) sind die Namen, auf die im Skript von ``scriptMap`` +verwiesen werden kann. ``DataStoreException`` ist eine Klasse aus dem Paket +``org.codelibs.fess.exception``. -.. code-block:: java +Unterstützung für Paginierung +------------------------------- - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +Beim Umgang mit großen Datenmengen erfolgt die Verarbeitung seitenweise. Wenn +die Verarbeitung pro Datensatz (Erstellen von ``resultMap``, Auswerten von +``scriptMap``, Aufruf von ``callback.store()``) in eine Methode wie +``processRecord()`` ausgelagert wird, lässt sie sich von der Abruflogik +trennen. - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); +.. code-block:: java - if (documents.isEmpty()) { - break; - } + int offset = 0; + final int pageSize = 100; - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); - page++; + if (records.isEmpty()) { + break; } + + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); + } + + offset += pageSize; } -Authentifizierung -================= +Implementierung der Authentifizierung +======================================== + +Die Authentifizierung gegenüber dem externen System wird auf Seiten des +Konnektors implementiert. Das folgende Beispiel verwendet eine gängige +HTTP-Client-Bibliothek und ist keine von |Fess| bereitgestellte API. Binden Sie +die verwendete Bibliothek als Abhängigkeit des Plugins ein. OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // Token-Aktualisierung - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // Beispiel zum Abrufen eines Access-Tokens mittels Refresh-Token + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } API-Key-Authentifizierung -------------------------- +--------------------------- .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -196,7 +323,10 @@ API-Key-Authentifizierung } Fehlerbehandlung -================ +================== + +Bei fatalen Fehlern, die die Verarbeitung abbrechen sollen, wird +``DataStoreException`` geworfen. .. code-block:: java @@ -204,76 +334,222 @@ Fehlerbehandlung protected void storeData(...) { try { // Verarbeitung - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); + sleep(60000L); // Retry-Logik - } catch (Exception e) { + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + Bei tatsächlichen Konnektoren wie ``fess-ds-example`` wird + ``CrawlingAccessException`` pro Datensatz abgefangen, damit ein Fehler bei + einem einzelnen Datensatz nicht den gesamten Crawl-Vorgang stoppt; die + fehlerhafte URL wird dabei bei ``FailureUrlService`` protokolliert. Außerdem + wird über das Abbruch-Flag von ``DataStoreCrawlingException`` gesteuert, ob + der gesamte Crawl-Vorgang abgebrochen werden soll. Wenn Sie einen robusten + Konnektor implementieren möchten, orientieren Sie sich an der + Implementierung von ``ExampleDataStore``. + Tests ===== Unit-Tests ---------- +|Fess|-Plugins werden mit ``LastaDiTestCase`` aus UTFlute getestet. Die Tests +werden mit JUnit 5 (Jupiter) geschrieben. Indem ``IndexUpdateCallback`` durch +eine Implementierung ersetzt wird, die die registrierten ``dataMap``-Werte +sammelt, lässt sich das Mapping-Ergebnis ohne Mock-Bibliothek überprüfen. + .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { + + public ExampleDataStore dataStore; - private ExampleDataStore dataStore; + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } - @Before - public void setUp() { + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // Test mit Mocks + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // Test-Callback, der die registrierten dataMap-Werte sammelt + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // Keine Aktion + } + + public List> getDataMapList() { + return dataMapList; + } } } +.. note:: + + Da ``setUp`` in der Basisklasse bereits mit ``@BeforeEach`` versehen ist, + muss beim Überschreiben keine Lifecycle-Annotation erneut hinzugefügt + werden. Jeder Testmethode wird ``@Test`` (``org.junit.jupiter.api.Test``) + hinzugefügt. + +Build und Installation +======================== + +pom.xml +------- + +Das Plugin wird als jar mit ``fess-parent`` als übergeordnetem POM gebaut. Die +Abhängigkeiten von ``fess`` und ``opensearch`` werden zur Laufzeit von |Fess| +selbst bereitgestellt und daher als ``provided`` deklariert. + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +Für Tests werden JUnit 5 und ``org.dbflute.utflute:utflute-lastaflute`` +verwendet. + +Build +----- + +.. code-block:: bash + + mvn clean package + +Im Verzeichnis ``target/`` wird ``fess-ds-example-15.8.0.jar`` erzeugt. + +Installation +------------ + +Installieren Sie das erzeugte JAR in |Fess| und starten Sie |Fess| neu. +Details zum Installationsvorgang finden Sie unter +:doc:`../admin/plugin-guide`. Erstellen Sie nach der Installation über +„Crawler > Datenspeicher" in der Administrationsoberfläche eine neue +Konfiguration und geben Sie als „Handler-Name" den von ``getName()`` +zurückgegebenen Namen an (in diesem Beispiel ``ExampleDataStore``). + Konfigurationsbeispiel -====================== +======================== -Konfigurationsbeispiel in der Administrationsoberflache: +Konfigurationsbeispiel in der Administrationsoberfläche: Parameter --------- +Im Feld „Parameter" werden die Schlüssel und Werte angegeben, die der +Konnektor aus ``paramMap`` ausliest. + :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 Skript ------ +Im Feld „Skript" wird das Mapping im Format ``linke Seite=rechte Seite`` +angegeben. Die linke Seite ist der Indexfeldname, die rechte Seite ein Skript +(standardmäßig Groovy), das auf ein Feld des Quelldatensatzes verweist. Das +folgende Beispiel gilt für den Fall, dass der Quelldatensatz die Felder +``url`` / ``title`` / ``content`` / ``updated_at`` / ``content_type`` besitzt. + :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + Welche Feldnamen auf der rechten Seite referenziert werden können, hängt + von den Werten ab, die der Konnektor in ``resultMap`` ablegt (Werte aus + ``paramMap`` und Felder des Quelldatensatzes). Bei bestehenden Konnektoren + wie CSV oder JSON kann ein eigenes Präfix wie ``data.*`` vorangestellt + sein; weitere Details finden Sie in der Dokumentation des jeweiligen + Konnektors. Referenzinformationen -===================== +====================== - :doc:`plugin-architecture` - Plugin-Architektur -- :doc:`../config/datastore/ds-overview` - DataStore-Konnektor-Übersicht -- `GitHub: fess-ds-* `__ - Beispiele fur veroffentlichte Plugins +- :doc:`../admin/plugin-guide` - Plugin-Installation +- :doc:`../config/datastore/ds-overview` - Übersicht der Datenspeicher-Konnektoren +- `fess-ds-example `__ - Implementierungsvorlage für DataStore-Plugins +- `GitHub: fess-ds-* `__ - Beispiele veröffentlichter Konnektoren diff --git a/en/15.8/dev/datastore-plugin.rst b/en/15.8/dev/datastore-plugin.rst index 6d5f41240..f29e460c5 100644 --- a/en/15.8/dev/datastore-plugin.rst +++ b/en/15.8/dev/datastore-plugin.rst @@ -1,17 +1,30 @@ ================================== -DataStore Plugin Development +Data Store Plugin Development ================================== Overview ======== -By developing a DataStore plugin, you can add content retrieval functionality -from new data sources to |Fess|. +By developing a Data Store plugin, you can add functionality to |Fess| for +retrieving content from new data sources. A Data Store retrieves records from +external systems such as databases, APIs, and files, converts them into index +fields according to the mapping script configured in the admin console, and +registers them into the |Fess| index. + +Published connectors (``fess-ds-*``) for CSV, JSON, databases, Git, and +various cloud services are all implemented using this mechanism. Because +`fess-ds-example `__ is published +as an implementation template, the easiest way to create a new connector is +to copy it as a starting point. Basic Structure =============== -DataStore plugins extend ``AbstractDataStore``. +A Data Store plugin consists of the following three parts: + +- Create a class that extends ``AbstractDataStore`` +- Implement the two methods ``getName()`` and ``storeData()`` +- Register it as a component in ``fess_ds++.xml`` Minimum Implementation ---------------------- @@ -24,27 +37,57 @@ Minimum Implementation import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // Used as the handler name. Convention is to return the class's simple name + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { // Implement data retrieval and processing here } } +.. note:: + + Both ``getName()`` and ``storeData()`` are ``protected`` abstract methods. + Note that in |Fess| 15.x, the package for ``DataConfig`` is + ``org.codelibs.fess.opensearch.config.exentity`` (the former + ``org.codelibs.fess.es.config.exentity`` has been removed). + +Component Registration +---------------------- + +To make |Fess| recognize the Data Store you created, register the component +in ``src/main/resources/fess_ds++.xml``. + +.. code-block:: xml + + + + + + + + + +With ````, the ``register()`` method inherited +from ``AbstractDataStore`` is automatically invoked after the component is +created, registering itself with the ``DataStoreFactory``. The name +registered at this time is the return value of ``getName()`` (``ExampleDataStore`` +in the example above), and this becomes the "handler name" selected in the +Data Store settings in the admin console. + AbstractDataStore ================= @@ -53,130 +96,219 @@ Main Methods .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 * - Method + - Category - Description * - ``getName()`` - - Returns the DataStore name (required) + - Implementation (required) + - Abstract method that returns the Data Store's handler name. Convention + is to return ``getClass().getSimpleName()`` * - ``storeData()`` - - Performs data retrieval and index registration (required) + - Implementation (required) + - Abstract method that performs data retrieval, conversion, and index + registration * - ``register()`` - - Registers the plugin - -Parameters ----------- + - Inherited (normally no changes needed) + - Automatically invoked from the ``postConstruct`` in ``fess_ds++.xml`` + and registers with ``DataStoreFactory`` + * - ``store()`` + - Inherited (called by the framework) + - The entry point invoked by the framework. Prepares ``defaultDataMap`` + and other data, then calls ``storeData()`` + * - ``convertValue()`` + - Inherited (helper) + - Evaluates the value (template) in ``scriptMap`` using the script engine + * - ``getScriptType()`` + - Inherited (helper) + - Retrieves the ``script_type`` parameter (default is Groovy) + * - ``getReadInterval()`` + - Inherited (helper) + - Retrieves the ``readInterval`` parameter (in milliseconds) + * - ``sleep()`` + - Inherited (helper) + - Sleeps for the specified number of milliseconds (used to wait between + records) + +storeData Parameters +-------------------- Parameters passed to the ``storeData()`` method: -- ``dataConfig``: DataStore configuration -- ``callback``: Index update callback -- ``paramMap``: Parameters configured in admin console -- ``scriptMap``: Script configuration -- ``defaultDataMap``: Default data map +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - Parameter + - Type + - Description + * - ``dataConfig`` + - ``DataConfig`` + - Data Store configuration (ID, handler name, parameters, scripts, etc.) + * - ``callback`` + - ``IndexUpdateCallback`` + - Callback for registering generated documents to the index + * - ``paramMap`` + - ``DataStoreParams`` + - Configuration values from the "Parameters" field in the admin console. + Access them using ``getAsString(key)`` / ``getAsString(key, default)`` / + ``get(key)`` / ``asMap()`` / ``containsKey(key)`` + * - ``scriptMap`` + - ``Map`` + - Configuration from the "Scripts" field in the admin console. The key + is the index field name, and the value is the script template to be + evaluated + * - ``defaultDataMap`` + - ``Map`` + - Default field values for each document (config ID, boost, role, + mimetype, virtual host, etc.), prepared by the framework + +.. warning:: + + The type of ``paramMap`` is ``DataStoreParams``, not + ``Map``. Since ``DataStoreParams`` does not implement + ``Map``, use ``getAsString()``, which returns a string, instead of + ``get()`` to retrieve values. + +Data Processing Flow +-------------------- + +The implementation of ``storeData()`` processes data in the following flow. + +#. Retrieve source records from the external system. +#. Merge the source record's fields into ``paramMap.asMap()`` to build + ``resultMap`` (scripts are evaluated against this ``resultMap``). +#. Evaluate each entry in ``scriptMap`` with + ``convertValue(scriptType, template, resultMap)`` and store the result in + ``dataMap``. The important point is that the mapping is not hardcoded in + the code, but defined by the administrator in the "Scripts" field. +#. Call ``callback.store(paramMap, dataMap)`` to register the document to + the index. Implementation Examples ======================= -Simple DataStore ----------------- +Simple Data Store +------------------ + +An example of retrieving records from an external API and registering them +to the index. .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // Get parameters - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // Get parameters (use getAsString with DataStoreParams) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // Script evaluation type (default is Groovy) + final String scriptType = getScriptType(paramMap); + // Wait time between records (milliseconds) + final long readInterval = getReadInterval(paramMap); try { - // Fetch data - List documents = fetchDocuments(apiUrl, apiKey); + // Retrieve data from the external system + final List> records = fetchRecords(apiUrl, apiKey); - // Process each document - for (Document doc : documents) { + for (final Map record : records) { + // Copy default fields to initialize dataMap final Map dataMap = new HashMap<>(defaultDataMap); - // Map data - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // Build resultMap by merging parameters and the source record + // (scripts are evaluated against this resultMap) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // Execute scripts (mapping) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // Evaluate each entry in scriptMap to generate index fields + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } - // Register to index - callback.store(paramMap, resultMap); + // Register to the index + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } +``fetchRecords()`` is a custom method that retrieves a list of records from +the external system. The field names of each retrieved record +(``Map``) become the names that can be referenced from the +scripts in ``scriptMap``. ``DataStoreException`` is a class in the +``org.codelibs.fess.exception`` package. + Pagination Support ------------------- +------------------- -.. code-block:: java +When handling large volumes of data, process the data page by page while +retrieving it. By extracting the per-record processing (building +``resultMap``, evaluating ``scriptMap``, and calling ``callback.store()``) +into a method such as ``processRecord()``, you can separate it from the +retrieval logic. - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +.. code-block:: java - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); + int offset = 0; + final int pageSize = 100; - if (documents.isEmpty()) { - break; - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + if (records.isEmpty()) { + break; + } - page++; + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); } + + offset += pageSize; } Authentication Implementation ============================= +Authentication with the external system is implemented on the connector +side. The following is an example implementation using a common HTTP client +library; it is not an API provided by |Fess|. Include the library you use +as a dependency of the plugin. + OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // Token refresh - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // Example of obtaining an access token using a refresh token + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } @@ -186,8 +318,8 @@ API Key Authentication .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -198,83 +330,226 @@ API Key Authentication Error Handling ============== +For fatal errors that should abort processing, throw ``DataStoreException``. + .. code-block:: java @Override protected void storeData(...) { try { // Processing - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); + sleep(60000L); // Retry logic - } catch (Exception e) { + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + In actual connectors such as ``fess-ds-example``, to avoid stopping the + entire crawl due to an error in a single record, ``CrawlingAccessException`` + is caught per record and the error URL is recorded in + ``FailureUrlService``. The interrupt flag of ``DataStoreCrawlingException`` + is also used to control whether to abort the entire crawl. When + implementing a robust connector, refer to the implementation of + ``ExampleDataStore``. + Testing ======= Unit Testing ------------ +|Fess| plugins are tested using UTFlute's ``LastaDiTestCase``. Tests are +written in JUnit 5 (Jupiter). By replacing ``IndexUpdateCallback`` with an +implementation that collects the registered ``dataMap``, you can verify the +mapping results without using a mock library. + .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { + + public ExampleDataStore dataStore; - private ExampleDataStore dataStore; + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } - @Before - public void setUp() { + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // Test using mocks + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // Test callback that collects the registered dataMap + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // Do nothing + } + + public List> getDataMapList() { + return dataMapList; + } } } +.. note:: + + Since ``setUp`` is annotated with ``@BeforeEach`` in the base class, there + is no need to re-annotate the lifecycle annotation on the overriding side. + Add ``@Test`` (``org.junit.jupiter.api.Test``) to each test method. + +Build and Installation +======================= + +pom.xml +------- + +The plugin is built as a jar with ``fess-parent`` as the parent POM. +Dependencies on ``fess`` and ``opensearch`` are set to ``provided`` because +they are supplied by the |Fess| core at runtime. + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +JUnit 5 and ``org.dbflute.utflute:utflute-lastaflute`` are used for testing. + +Build +----- + +.. code-block:: bash + + mvn clean package + +``fess-ds-example-15.8.0.jar`` is generated in the ``target/`` directory. + +Installation +------------ + +Install the generated JAR into |Fess| and restart |Fess|. For details on the +installation procedure, see :doc:`../admin/plugin-guide`. After installation, +create a new configuration from "Crawler > Data Store" in the admin console, +and specify the name returned by ``getName()`` (``ExampleDataStore`` in this +example) as the "handler name". + Configuration Example -===================== +====================== -Example configuration in admin console: +Example configuration in the admin console: Parameters ---------- +In the "Parameters" field, describe the keys and values that the connector +reads from ``paramMap``. + :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 Scripts ------- +In the "Scripts" field, describe the mapping in the form +``left-hand side=right-hand side``. The left-hand side is the index field +name, and the right-hand side is a script (Groovy by default) that +references a field of the source record. The following is an example where +the source record has the ``url`` / ``title`` / ``content`` / +``updated_at`` / ``content_type`` fields. + :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + The field names that can be referenced on the right-hand side depend on + the values that the connector stores in ``resultMap`` (the values of + ``paramMap`` and the fields of the source record). Existing connectors + such as CSV and JSON may use a custom prefix such as ``data.*``, so refer + to the documentation for each connector. Reference ========= - :doc:`plugin-architecture` - Plugin Architecture -- :doc:`../config/datastore/ds-overview` - DataStore Connector Overview -- `GitHub: fess-ds-* `__ - Published plugin examples - +- :doc:`../admin/plugin-guide` - Plugin Installation +- :doc:`../config/datastore/ds-overview` - Data Store Connector Overview +- `fess-ds-example `__ - Data Store plugin implementation template +- `GitHub: fess-ds-* `__ - Examples of published connectors diff --git a/es/15.8/dev/datastore-plugin.rst b/es/15.8/dev/datastore-plugin.rst index e72dd3aab..d1591320b 100644 --- a/es/15.8/dev/datastore-plugin.rst +++ b/es/15.8/dev/datastore-plugin.rst @@ -1,19 +1,34 @@ -================================== -Desarrollo de Plugins de Almacen de Datos -================================== +========================================= +Desarrollo de Plugins de Almacén de Datos +========================================= -Vision General +Visión General ============== -Al desarrollar un plugin de almacen de datos, puede agregar funcionalidad a |Fess| -para obtener contenido desde nuevas fuentes de datos. +Al desarrollar un plugin de almacén de datos, puede agregar a |Fess| la +funcionalidad de obtener contenido desde nuevas fuentes de datos. El almacén +de datos obtiene registros de sistemas externos, como bases de datos, API o +archivos, los convierte en campos de índice de acuerdo con el script de +mapeo configurado en la consola de administración, y los registra en el +índice de |Fess|. -Estructura Basica -================= +Todos los conectores públicos (``fess-ds-*``), como CSV, JSON, bases de +datos, Git y diversos servicios en la nube, están implementados con este +mecanismo. Como plantilla de implementación se publica +`fess-ds-example `__, por lo +que, al crear un nuevo conector, resulta sencillo comenzar copiando este +proyecto. + +Estructura Básica +================== -El plugin de almacen de datos se implementa heredando de ``AbstractDataStore``. +El plugin de almacén de datos se compone de los siguientes tres elementos: -Implementacion Minima +- Crear una clase que herede de ``AbstractDataStore`` +- Implementar los dos métodos ``getName()`` y ``storeData()`` +- Registrarla como componente en ``fess_ds++.xml`` + +Implementación Mínima --------------------- .. code-block:: java @@ -24,170 +39,297 @@ Implementacion Minima import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // Se utiliza como nombre del manejador. La convención es devolver el nombre simple de la clase + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // Implementar la obtencion y procesamiento de datos aqui + // Implementar aquí la obtención y el procesamiento de los datos } } +.. note:: + + Tanto ``getName()`` como ``storeData()`` son métodos abstractos + ``protected``. Tenga en cuenta que, en |Fess| 15.x, el paquete de + ``DataConfig`` es ``org.codelibs.fess.opensearch.config.exentity`` (el + anterior ``org.codelibs.fess.es.config.exentity`` ha quedado obsoleto). + +Registro del Componente +----------------------- + +Para que |Fess| reconozca el almacén de datos creado, registre el +componente en ``src/main/resources/fess_ds++.xml``. + +.. code-block:: xml + + + + + + + + + +Mediante ````, tras la creación del +componente se invoca automáticamente el método ``register()`` que posee +``AbstractDataStore``, y este se registra a sí mismo en +``DataStoreFactory``. El nombre registrado en este momento es el valor +devuelto por ``getName()`` (``ExampleDataStore`` en el ejemplo anterior), y +es el que se selecciona como «nombre del manejador» en la configuración del +almacén de datos de la consola de administración. + AbstractDataStore ================= -Metodos Principales -------------------- +Métodos Principales +-------------------- .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 - * - Metodo - - Descripcion + * - Método + - Categoría + - Descripción * - ``getName()`` - - Devuelve el nombre del almacen de datos (obligatorio) + - Implementación (obligatoria) + - Método abstracto que devuelve el nombre del manejador del almacén de + datos. La convención es devolver ``getClass().getSimpleName()`` * - ``storeData()`` - - Realiza la obtencion de datos y registro en el indice (obligatorio) + - Implementación (obligatoria) + - Método abstracto que obtiene, convierte y registra los datos en el + índice * - ``register()`` - - Registra el plugin - -Parametros ----------- - -Parametros pasados al metodo ``storeData()``: - -- ``dataConfig``: Configuracion del almacen de datos -- ``callback``: Callback para actualizacion del indice -- ``paramMap``: Parametros configurados en la pantalla de administracion -- ``scriptMap``: Configuracion de scripts -- ``defaultDataMap``: Mapa de datos predeterminado + - Heredado (normalmente no requiere cambios) + - Se invoca automáticamente desde el ``postConstruct`` de + ``fess_ds++.xml`` y se registra en ``DataStoreFactory`` + * - ``store()`` + - Heredado (invocado por el framework) + - Punto de entrada invocado por el framework. Prepara + ``defaultDataMap`` y otros datos, y llama a ``storeData()`` + * - ``convertValue()`` + - Heredado (auxiliar) + - Evalúa con el motor de scripts el valor (plantilla) de ``scriptMap`` + * - ``getScriptType()`` + - Heredado (auxiliar) + - Obtiene el parámetro ``script_type`` (el valor predeterminado es + Groovy) + * - ``getReadInterval()`` + - Heredado (auxiliar) + - Obtiene el parámetro ``readInterval`` (en milisegundos) + * - ``sleep()`` + - Heredado (auxiliar) + - Duerme durante los milisegundos indicados (se utiliza para esperar + entre registros) + +Parámetros de storeData +----------------------- -Ejemplo de Implementacion -========================= +Parámetros que se pasan al método ``storeData()``: -Almacen de Datos Simple +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - Parámetro + - Tipo + - Descripción + * - ``dataConfig`` + - ``DataConfig`` + - Configuración del almacén de datos (ID, nombre del manejador, + parámetros, scripts, etc.) + * - ``callback`` + - ``IndexUpdateCallback`` + - Callback para registrar en el índice los documentos generados + * - ``paramMap`` + - ``DataStoreParams`` + - Valores configurados en el campo «Parámetros» de la consola de + administración. Se accede a ellos mediante ``getAsString(key)``, + ``getAsString(key, default)``, ``get(key)``, ``asMap()`` y + ``containsKey(key)`` + * - ``scriptMap`` + - ``Map`` + - Configuración del campo «Script» de la consola de administración. + La clave es el nombre del campo de índice, y el valor es la + plantilla de script que se evalúa + * - ``defaultDataMap`` + - ``Map`` + - Valores predeterminados de campo para cada documento (ID de + configuración, boost, role, mimetype, host virtual, etc.). El + framework los prepara + +.. warning:: + + El tipo de ``paramMap`` no es ``Map``, sino + ``DataStoreParams``. Dado que ``DataStoreParams`` no implementa + ``Map``, utilice ``getAsString()``, que devuelve una cadena, en lugar + de ``get()`` para obtener los valores. + +Flujo de Procesamiento de Datos +-------------------------------- + +La implementación de ``storeData()`` procesa los datos siguiendo este +flujo. + +#. Obtener los registros de origen desde el sistema externo. +#. Combinar los campos del registro de origen con ``paramMap.asMap()`` + para construir ``resultMap`` (el script se evalúa sobre este + ``resultMap``). +#. Evaluar cada entrada de ``scriptMap`` con + ``convertValue(scriptType, template, resultMap)`` y almacenar el + resultado en ``dataMap``. Es importante que el mapeo no se codifique de + forma fija en el código, sino que lo defina el administrador en el + campo «Script». +#. Invocar ``callback.store(paramMap, dataMap)`` para registrar el + documento en el índice. + +Ejemplo de Implementación +========================== + +Almacén de Datos Simple ----------------------- +Ejemplo que obtiene registros desde una API externa y los registra en el +índice. + .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // Obtener parametros - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // Obtener los parámetros (en DataStoreParams se usa getAsString) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // Tipo de evaluación del script (el valor predeterminado es Groovy) + final String scriptType = getScriptType(paramMap); + // Tiempo de espera entre registros (milisegundos) + final long readInterval = getReadInterval(paramMap); try { - // Obtener datos - List documents = fetchDocuments(apiUrl, apiKey); + // Obtener los datos del sistema externo + final List> records = fetchRecords(apiUrl, apiKey); - // Procesar cada documento - for (Document doc : documents) { + for (final Map record : records) { + // Copiar los campos predeterminados para inicializar dataMap final Map dataMap = new HashMap<>(defaultDataMap); - // Mapeo de datos - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // Construir resultMap combinando los parámetros y el registro de origen + // (el script se evalúa sobre este resultMap) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // Ejecutar script (mapeo) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // Evaluar cada entrada de scriptMap para generar los campos de índice + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } - // Registrar en el indice - callback.store(paramMap, resultMap); + // Registrar en el índice + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } -Soporte de Paginacion ---------------------- +``fetchRecords()`` es un método propio que obtiene la lista de registros +del sistema externo. Los nombres de los campos de cada registro obtenido +(``Map``) son los nombres que se pueden referenciar desde +los scripts de ``scriptMap``. ``DataStoreException`` es una clase del +paquete ``org.codelibs.fess.exception``. -.. code-block:: java +Soporte de Paginación +---------------------- - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +Cuando se manejan grandes volúmenes de datos, el procesamiento se realiza +obteniendo los registros página por página. Si se extrae el procesamiento +por registro (la construcción de ``resultMap``, la evaluación de +``scriptMap`` y la llamada a ``callback.store()``) a un método como +``processRecord()``, es posible separarlo de la lógica de obtención. - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); +.. code-block:: java - if (documents.isEmpty()) { - break; - } + int offset = 0; + final int pageSize = 100; - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); + + if (records.isEmpty()) { + break; + } - page++; + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); } + + offset += pageSize; } -Implementacion de Autenticacion -=============================== +Implementación de Autenticación +================================ + +La autenticación con el sistema externo se implementa en el lado del +conector. A continuación se muestra un ejemplo de implementación con una +biblioteca cliente HTTP habitual; no se trata de una API proporcionada por +|Fess|. Incluya la biblioteca que utilice como dependencia del plugin. OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // Refrescar el token - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // Ejemplo de obtención de un token de acceso mediante el token de actualización + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } -Autenticacion con Clave API ---------------------------- +Autenticación con Clave API +---------------------------- .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -198,82 +340,236 @@ Autenticacion con Clave API Manejo de Errores ================= +Para errores fatales que deben interrumpir el procesamiento, lance +``DataStoreException``. + .. code-block:: java @Override protected void storeData(...) { try { // Procesamiento - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); - // Logica de reintento - } catch (Exception e) { + sleep(60000L); + // Lógica de reintento + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + En conectores reales, como ``fess-ds-example``, para evitar que el + error de un solo registro detenga todo el rastreo, se captura + ``CrawlingAccessException`` a nivel de registro y se registra la URL + con error en ``FailureUrlService``. Además, se controla si se debe + interrumpir todo el rastreo mediante el indicador de interrupción de + ``DataStoreCrawlingException``. Si desea implementar un conector + robusto, consulte la implementación de ``ExampleDataStore``. + Pruebas ======= Pruebas Unitarias ------------------ +------------------ + +Los plugins de |Fess| se prueban utilizando ``LastaDiTestCase`` de +UTFlute. Las pruebas se escriben con JUnit 5 (Jupiter). Al sustituir +``IndexUpdateCallback`` por una implementación que recopila los +``dataMap`` registrados, es posible verificar el resultado del mapeo sin +utilizar bibliotecas de mocks. .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { - private ExampleDataStore dataStore; + public ExampleDataStore dataStore; - @Before - public void setUp() { + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } + + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // Pruebas usando mocks + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // Callback de prueba que recopila los dataMap registrados + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // No hace nada + } + + public List> getDataMapList() { + return dataMapList; + } } } -Ejemplo de Configuracion +.. note:: + + ``setUp`` ya tiene la anotación ``@BeforeEach`` en la clase base, por + lo que no es necesario volver a añadir anotaciones de ciclo de vida al + sobrescribirlo. Cada método de prueba debe llevar la anotación + ``@Test`` (``org.junit.jupiter.api.Test``). + +Construcción e Instalación +=========================== + +pom.xml +------- + +El plugin se construye como un jar que utiliza ``fess-parent`` como POM +padre. Las dependencias de ``fess`` y ``opensearch`` se marcan como +``provided``, ya que en tiempo de ejecución las proporciona el propio +|Fess|. + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +Para las pruebas se utilizan JUnit 5 y +``org.dbflute.utflute:utflute-lastaflute``. + +Construcción +------------ + +.. code-block:: bash + + mvn clean package + +Se generará ``fess-ds-example-15.8.0.jar`` en el directorio ``target/``. + +Instalación +----------- + +Instale el JAR generado en |Fess| y reinicie |Fess|. Para más detalles +sobre el procedimiento de instalación, consulte +:doc:`../admin/plugin-guide`. Después de la instalación, cree una nueva +configuración desde «Rastreador > Almacén de Datos» en la consola de +administración y especifique en «Nombre del manejador» el nombre que +devuelve ``getName()`` (``ExampleDataStore`` en este ejemplo). + +Ejemplo de Configuración ======================== -Ejemplo de configuracion en la pantalla de administracion: +Ejemplo de configuración en la consola de administración: -Parametros +Parámetros ---------- +En el campo «Parámetros» se describen las claves y los valores que el +conector lee desde ``paramMap``. + :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 Script ------ +En el campo «Script» se describe el mapeo con el formato +``lado izquierdo=lado derecho``. El lado izquierdo es el nombre del campo +de índice, y el lado derecho es un script (Groovy de forma predeterminada) +que referencia un campo del registro de origen. A continuación se muestra +un ejemplo para el caso en que el registro de origen tiene los campos +``url``, ``title``, ``content``, ``updated_at`` y ``content_type``. + :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + Los nombres de campo que se pueden referenciar en el lado derecho + dependen de los valores que el conector almacena en ``resultMap`` (los + valores de ``paramMap`` y los campos del registro de origen). En + conectores existentes, como CSV o JSON, puede añadirse un prefijo + propio como ``data.*``, por lo que debe consultar la documentación de + cada conector. -Informacion de Referencia -========================= +Información de Referencia +========================== - :doc:`plugin-architecture` - Arquitectura de plugins -- :doc:`../config/datastore/ds-overview` - Vision general de conectores de almacen de datos -- `GitHub: fess-ds-* `__ - Ejemplos de plugins publicos +- :doc:`../admin/plugin-guide` - Instalación de plugins +- :doc:`../config/datastore/ds-overview` - Descripción general de los + conectores de almacén de datos +- `fess-ds-example `__ - + Plantilla de implementación de plugins de almacén de datos +- `GitHub: fess-ds-* `__ - Ejemplos + de conectores públicos diff --git a/fr/15.8/dev/datastore-plugin.rst b/fr/15.8/dev/datastore-plugin.rst index dc8d9968b..0c92226bd 100644 --- a/fr/15.8/dev/datastore-plugin.rst +++ b/fr/15.8/dev/datastore-plugin.rst @@ -1,20 +1,34 @@ ================================== -Developpement de plugins DataStore +Développement de plugins DataStore ================================== -Vue d'ensemble -============== +Aperçu +====== -En developpant un plugin DataStore, vous pouvez ajouter a |Fess| la capacite -de recuperer du contenu depuis de nouvelles sources de donnees. +En développant un plugin DataStore, vous pouvez ajouter à |Fess| la capacité +de récupérer du contenu depuis de nouvelles sources de données. Un DataStore +récupère des enregistrements depuis un système externe (base de données, API, +fichiers, etc.), les convertit en champs d'index selon le script de mapping +défini dans l'écran d'administration, puis les enregistre dans l'index de +|Fess|. + +Les connecteurs publics tels que CSV, JSON, base de données, Git et divers +services cloud (``fess-ds-*``) sont tous implémentés selon ce mécanisme. +`fess-ds-example `__ est publié +comme modèle d'implémentation ; pour créer un nouveau connecteur, il est donc +plus simple de partir en copiant ce projet. Structure de base ================= -Un plugin DataStore herite de ``AbstractDataStore``. +Un plugin DataStore se compose des trois éléments suivants. + +- créer une classe héritant de ``AbstractDataStore`` +- implémenter les deux méthodes ``getName()`` et ``storeData()`` +- l'enregistrer comme composant dans ``fess_ds++.xml`` -Implementation minimale ------------------------ +Implémentation minimale +------------------------ .. code-block:: java @@ -24,170 +38,278 @@ Implementation minimale import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // Utilisé comme nom de handler. Il est d'usage de retourner le nom simple de la classe + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // Implementer la recuperation et le traitement des donnees ici + // Implémenter ici la récupération et le traitement des données } } +.. note:: + + ``getName()`` et ``storeData()`` sont tous deux des méthodes abstraites + ``protected``. Notez que le package de ``DataConfig`` dans |Fess| 15.x est + ``org.codelibs.fess.opensearch.config.exentity`` (l'ancien + ``org.codelibs.fess.es.config.exentity`` a été supprimé). + +Enregistrement du composant +---------------------------- + +Pour que |Fess| reconnaisse le DataStore créé, enregistrez le composant dans +``src/main/resources/fess_ds++.xml``. + +.. code-block:: xml + + + + + + + + + +Grâce à ````, la méthode ``register()`` fournie +par ``AbstractDataStore`` est appelée automatiquement après la création du +composant, ce qui enregistre celui-ci auprès de ``DataStoreFactory``. Le nom +enregistré à ce moment est la valeur de retour de ``getName()`` (``ExampleDataStore`` +dans l'exemple ci-dessus), qui devient le « nom de handler » sélectionnable dans +la configuration du DataStore de l'écran d'administration. + AbstractDataStore ================= -Methodes principales --------------------- +Méthodes principales +--------------------- .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 - * - Methode + * - Méthode + - Catégorie - Description * - ``getName()`` - - Retourne le nom du DataStore (obligatoire) + - Implémentation (obligatoire) + - Méthode abstraite retournant le nom de handler du DataStore. Il est d'usage de retourner ``getClass().getSimpleName()`` * - ``storeData()`` - - Effectue la recuperation et l'enregistrement dans l'index (obligatoire) + - Implémentation (obligatoire) + - Méthode abstraite effectuant la récupération, la conversion et l'enregistrement des données dans l'index * - ``register()`` - - Enregistre le plugin - -Parametres ----------- + - Héritée (généralement inchangée) + - Appelée automatiquement depuis le ``postConstruct`` de ``fess_ds++.xml`` ; enregistre le composant auprès de ``DataStoreFactory`` + * - ``store()`` + - Héritée (appelée par le framework) + - Point d'entrée appelé par le framework. Prépare notamment ``defaultDataMap`` puis appelle ``storeData()`` + * - ``convertValue()`` + - Héritée (méthode d'aide) + - Évalue la valeur (le gabarit) de ``scriptMap`` à l'aide du moteur de script + * - ``getScriptType()`` + - Héritée (méthode d'aide) + - Récupère le paramètre ``script_type`` (Groovy par défaut) + * - ``getReadInterval()`` + - Héritée (méthode d'aide) + - Récupère le paramètre ``readInterval`` (en millisecondes) + * - ``sleep()`` + - Héritée (méthode d'aide) + - Met en pause l'exécution pendant le nombre de millisecondes indiqué (utilisé pour patienter entre les enregistrements) + +Paramètres de storeData +------------------------- + +Paramètres passés à la méthode ``storeData()`` : -Parametres passes a la methode ``storeData()``: - -- ``dataConfig``: Configuration du DataStore -- ``callback``: Callback pour la mise a jour de l'index -- ``paramMap``: Parametres configures dans l'interface d'administration -- ``scriptMap``: Configuration des scripts -- ``defaultDataMap``: Map de donnees par defaut +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 -Exemple d'implementation + * - Paramètre + - Type + - Description + * - ``dataConfig`` + - ``DataConfig`` + - Configuration du DataStore (ID, nom de handler, paramètres, scripts, etc.) + * - ``callback`` + - ``IndexUpdateCallback`` + - Callback permettant d'enregistrer le document généré dans l'index + * - ``paramMap`` + - ``DataStoreParams`` + - Valeurs configurées dans le champ « Paramètres » de l'écran d'administration. Accessible via ``getAsString(key)`` / ``getAsString(key, default)`` / ``get(key)`` / ``asMap()`` / ``containsKey(key)`` + * - ``scriptMap`` + - ``Map`` + - Configuration du champ « Script » de l'écran d'administration. La clé est le nom du champ d'index, la valeur est le gabarit de script à évaluer + * - ``defaultDataMap`` + - ``Map`` + - Valeurs de champs par défaut de chaque document (ID de configuration, boost, rôle, mimetype, hôte virtuel, etc.). Préparé par le framework + +.. warning:: + + Le type de ``paramMap`` n'est pas ``Map`` mais + ``DataStoreParams``. ``DataStoreParams`` n'implémentant pas ``Map``, + utilisez ``getAsString()``, qui retourne une chaîne, plutôt que ``get()`` + pour récupérer les valeurs. + +Flux de traitement des données +-------------------------------- + +L'implémentation de ``storeData()`` traite les données selon le flux suivant. + +#. Récupérer les enregistrements source depuis le système externe. +#. Construire ``resultMap`` en fusionnant les champs de l'enregistrement source + dans ``paramMap.asMap()`` (le script est évalué sur ce ``resultMap``). +#. Évaluer chaque entrée de ``scriptMap`` avec ``convertValue(scriptType, template, resultMap)`` + et stocker le résultat dans ``dataMap``. Il est important que le mapping ne + soit pas codé en dur dans le code, mais défini par l'administrateur dans le + champ « Script ». +#. Appeler ``callback.store(paramMap, dataMap)`` pour enregistrer le document dans l'index. + +Exemple d'implémentation ======================== DataStore simple ----------------- +----------------- + +Exemple récupérant des enregistrements depuis une API externe et les +enregistrant dans l'index. .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // Recuperation des parametres - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // Récupération des paramètres (utiliser getAsString avec DataStoreParams) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // Type d'évaluation du script (Groovy par défaut) + final String scriptType = getScriptType(paramMap); + // Délai d'attente entre les enregistrements (en millisecondes) + final long readInterval = getReadInterval(paramMap); try { - // Recuperation des donnees - List documents = fetchDocuments(apiUrl, apiKey); + // Récupération des données depuis le système externe + final List> records = fetchRecords(apiUrl, apiKey); - // Traitement de chaque document - for (Document doc : documents) { + for (final Map record : records) { + // Initialise dataMap en copiant les champs par défaut final Map dataMap = new HashMap<>(defaultDataMap); - // Mapping des donnees - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // Construit resultMap en fusionnant les paramètres et l'enregistrement source + // (le script est évalué sur ce resultMap) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // Execution du script (mapping) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // Évalue chaque entrée de scriptMap pour générer les champs d'index + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } // Enregistrement dans l'index - callback.store(paramMap, resultMap); + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } -Gestion de la pagination ------------------------- +``fetchRecords()`` est une méthode propre au connecteur qui récupère la liste +des enregistrements depuis le système externe. Les noms de champs de chaque +enregistrement récupéré (``Map``) sont les noms auxquels les +scripts de ``scriptMap`` peuvent faire référence. ``DataStoreException`` est +une classe du package ``org.codelibs.fess.exception``. -.. code-block:: java +Prise en charge de la pagination +----------------------------------- - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +Pour traiter de grands volumes de données, il convient de récupérer les +enregistrements page par page. Il est recommandé d'extraire le traitement par +enregistrement (construction de ``resultMap``, évaluation de ``scriptMap``, +appel de ``callback.store()``) dans une méthode telle que ``processRecord()``, +ce qui permet de séparer cette logique de celle de récupération. - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); +.. code-block:: java - if (documents.isEmpty()) { - break; - } + int offset = 0; + final int pageSize = 100; - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); + + if (records.isEmpty()) { + break; + } - page++; + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); } + + offset += pageSize; } -Implementation de l'authentification -==================================== +Implémentation de l'authentification +===================================== + +L'authentification auprès du système externe est implémentée côté connecteur. +Voici un exemple d'implémentation utilisant une bibliothèque cliente HTTP +courante ; il ne s'agit pas d'une API fournie par |Fess|. La bibliothèque +utilisée doit être incluse comme dépendance du plugin. OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // Rafraichissement du token - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // Exemple de récupération d'un jeton d'accès à l'aide d'un jeton de rafraîchissement + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } -Authentification par cle API ----------------------------- +Authentification par clé API +------------------------------ .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -196,7 +318,10 @@ Authentification par cle API } Gestion des erreurs -=================== +==================== + +Pour les erreurs fatales devant interrompre le traitement, levez une exception +``DataStoreException``. .. code-block:: java @@ -204,76 +329,219 @@ Gestion des erreurs protected void storeData(...) { try { // Traitement - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); - // Logique de retry - } catch (Exception e) { + sleep(60000L); + // Logique de nouvelle tentative + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + Dans les connecteurs réels tels que ``fess-ds-example``, afin qu'une erreur + sur un seul enregistrement n'interrompe pas l'ensemble de l'exploration, une + ``CrawlingAccessException`` est capturée au niveau de chaque enregistrement + et l'URL en erreur est enregistrée dans ``FailureUrlService``. Le drapeau + d'interruption de ``DataStoreCrawlingException`` est également utilisé pour + contrôler si l'ensemble de l'exploration doit être interrompu. Pour + implémenter un connecteur robuste, référez-vous à l'implémentation de + ``ExampleDataStore``. + Tests ===== Tests unitaires ---------------- +----------------- + +Les plugins |Fess| sont testés à l'aide de la classe ``LastaDiTestCase`` d'UTFlute. +Les tests sont écrits avec JUnit 5 (Jupiter). En remplaçant ``IndexUpdateCallback`` +par une implémentation qui collecte les ``dataMap`` enregistrés, il est possible +de vérifier le résultat du mapping sans recourir à une bibliothèque de mocks. .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { - private ExampleDataStore dataStore; + public ExampleDataStore dataStore; + + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } - @Before - public void setUp() { + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // Test avec des mocks + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // Callback de test collectant les dataMap enregistrés + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // Ne rien faire + } + + public List> getDataMapList() { + return dataMapList; + } } } -Exemple de configuration +.. note:: + + Comme la classe de base annote déjà ``setUp`` avec ``@BeforeEach``, il n'est + pas nécessaire de réajouter d'annotation de cycle de vie sur la méthode + surchargée. Chaque méthode de test doit être annotée avec ``@Test`` + (``org.junit.jupiter.api.Test``). + +Build et installation ======================== -Exemple de configuration dans l'interface d'administration: +pom.xml +------- -Parametres +Le plugin est construit comme un jar ayant ``fess-parent`` pour POM parent. +Les dépendances vers ``fess`` et ``opensearch`` étant fournies au moment de +l'exécution par |Fess| lui-même, elles doivent être déclarées en ``provided``. + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +Les tests utilisent JUnit 5 et ``org.dbflute.utflute:utflute-lastaflute``. + +Build +----- + +.. code-block:: bash + + mvn clean package + +Le fichier ``fess-ds-example-15.8.0.jar`` est généré dans le répertoire ``target/``. + +Installation +------------ + +Installez le fichier JAR généré dans |Fess| puis redémarrez |Fess|. Pour les +détails de la procédure d'installation, référez-vous à :doc:`../admin/plugin-guide`. +Après l'installation, créez une nouvelle configuration depuis « Crawler > +DataStore » dans l'écran d'administration, et indiquez dans « Nom de handler » +le nom retourné par ``getName()`` (``ExampleDataStore`` dans cet exemple). + +Exemple de configuration +========================== + +Exemple de configuration dans l'écran d'administration : + +Paramètres ---------- +Le champ « Paramètres » contient les clés et valeurs que le connecteur lit +depuis ``paramMap``. + :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 Script ------ +Le champ « Script » contient le mapping sous la forme ``membre gauche=membre droit``. +Le membre gauche est le nom du champ d'index, le membre droit est un script +(Groovy par défaut) référençant un champ de l'enregistrement source. Voici un +exemple pour un enregistrement source comportant les champs ``url`` / ``title`` +/ ``content`` / ``updated_at`` / ``content_type``. + :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + Les noms de champs référençables dans le membre droit dépendent des valeurs + que le connecteur stocke dans ``resultMap`` (les valeurs de ``paramMap`` et + les champs de l'enregistrement source). Dans les connecteurs existants tels + que CSV ou JSON, un préfixe propre tel que ``data.*`` peut être utilisé ; + référez-vous à la documentation de chaque connecteur. -Informations complementaires -============================ +Informations complémentaires +============================== - :doc:`plugin-architecture` - Architecture des plugins +- :doc:`../admin/plugin-guide` - Installation des plugins - :doc:`../config/datastore/ds-overview` - Vue d'ensemble des connecteurs DataStore -- `GitHub: fess-ds-* `__ - Exemples de plugins publies +- `fess-ds-example `__ - Modèle d'implémentation de plugin DataStore +- `GitHub: fess-ds-* `__ - Exemples de connecteurs publiés diff --git a/ja/15.8/dev/datastore-plugin.rst b/ja/15.8/dev/datastore-plugin.rst index a8f8ef32a..440c994d9 100644 --- a/ja/15.8/dev/datastore-plugin.rst +++ b/ja/15.8/dev/datastore-plugin.rst @@ -6,12 +6,23 @@ ==== データストアプラグインを開発することで、|Fess| に新しいデータソースからの -コンテンツ取得機能を追加できます。 +コンテンツ取得機能を追加できます。データストアは、データベース・API・ファイルなどの +外部システムからレコードを取得し、管理画面で設定したマッピングスクリプトに従って +インデックスフィールドへ変換したうえで、|Fess| のインデックスへ登録します。 + +CSV・JSON・データベース・Git・各種クラウドサービスなど、公開されている +コネクター(``fess-ds-*``)はすべてこの仕組みで実装されています。実装の雛形として +`fess-ds-example `__ が公開されているため、 +新しいコネクターを作成する場合はこれをコピーして始めるのが簡単です。 基本構造 ======== -データストアプラグインは ``AbstractDataStore`` を継承して実装します。 +データストアプラグインは、次の3点で構成されます。 + +- ``AbstractDataStore`` を継承したクラスを作成する +- ``getName()`` と ``storeData()`` の2つのメソッドを実装する +- ``fess_ds++.xml`` でコンポーネントとして登録する 最小実装 -------- @@ -24,27 +35,54 @@ import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // ハンドラー名として使用される。クラスの単純名を返すのが慣例 + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { // データの取得と処理をここに実装 } } +.. note:: + + ``getName()`` と ``storeData()`` はいずれも ``protected`` の抽象メソッドです。 + ``DataConfig`` のパッケージは |Fess| 15.x では ``org.codelibs.fess.opensearch.config.exentity`` + である点に注意してください(以前の ``org.codelibs.fess.es.config.exentity`` は廃止されています)。 + +コンポーネントの登録 +-------------------- + +作成したデータストアを |Fess| に認識させるため、``src/main/resources/fess_ds++.xml`` に +コンポーネントを登録します。 + +.. code-block:: xml + + + + + + + + + +```` により、コンポーネント生成後に ``AbstractDataStore`` +が持つ ``register()`` メソッドが自動的に呼び出され、``DataStoreFactory`` へ自身が登録されます。 +このとき登録される名前が ``getName()`` の戻り値(上記の例では ``ExampleDataStore``)であり、 +管理画面のデータストア設定で選択する「ハンドラー名」になります。 + AbstractDataStore ================= @@ -53,27 +91,82 @@ AbstractDataStore .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 * - メソッド + - 区分 - 説明 * - ``getName()`` - - データストアの名前を返す(必須) + - 実装(必須) + - データストアのハンドラー名を返す抽象メソッド。``getClass().getSimpleName()`` を返すのが慣例 * - ``storeData()`` - - データの取得とインデックス登録を行う(必須) + - 実装(必須) + - データの取得・変換・インデックス登録を行う抽象メソッド * - ``register()`` - - プラグインを登録する - -パラメーター ------------- + - 継承(通常は変更不要) + - ``fess_ds++.xml`` の ``postConstruct`` から自動呼び出しされ、``DataStoreFactory`` へ登録する + * - ``store()`` + - 継承(フレームワーク呼び出し) + - フレームワークが呼び出す入口。``defaultDataMap`` などを準備して ``storeData()`` を呼ぶ + * - ``convertValue()`` + - 継承(ヘルパー) + - ``scriptMap`` の値(テンプレート)をスクリプトエンジンで評価する + * - ``getScriptType()`` + - 継承(ヘルパー) + - ``script_type`` パラメーターを取得する(既定は Groovy) + * - ``getReadInterval()`` + - 継承(ヘルパー) + - ``readInterval`` パラメーター(ミリ秒)を取得する + * - ``sleep()`` + - 継承(ヘルパー) + - 指定ミリ秒スリープする(レコード間の待機に使用) + +storeData のパラメーター +------------------------ ``storeData()`` メソッドに渡されるパラメーター: -- ``dataConfig``: データストア設定 -- ``callback``: インデックス更新用コールバック -- ``paramMap``: 管理画面で設定されたパラメーター -- ``scriptMap``: スクリプト設定 -- ``defaultDataMap``: デフォルトのデータマップ +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - パラメーター + - 型 + - 説明 + * - ``dataConfig`` + - ``DataConfig`` + - データストア設定(ID・ハンドラー名・パラメーター・スクリプトなど) + * - ``callback`` + - ``IndexUpdateCallback`` + - 生成したドキュメントをインデックスへ登録するコールバック + * - ``paramMap`` + - ``DataStoreParams`` + - 管理画面「パラメーター」欄の設定値。``getAsString(key)`` / ``getAsString(key, default)`` / ``get(key)`` / ``asMap()`` / ``containsKey(key)`` でアクセスする + * - ``scriptMap`` + - ``Map`` + - 管理画面「スクリプト」欄の設定。キーがインデックスフィールド名、値が評価対象のスクリプトテンプレート + * - ``defaultDataMap`` + - ``Map`` + - 各ドキュメントの既定フィールド値(設定ID・boost・role・mimetype・仮想ホストなど)。フレームワークが用意する + +.. warning:: + + ``paramMap`` の型は ``Map`` ではなく ``DataStoreParams`` です。 + ``DataStoreParams`` は ``Map`` を実装していないため、値の取得には ``get()`` ではなく + 文字列を返す ``getAsString()`` を使用してください。 + +データ処理の流れ +---------------- + +``storeData()`` の実装は、次の流れでデータを処理します。 + +#. 外部システムからソースレコードを取得する。 +#. ``paramMap.asMap()`` にソースレコードのフィールドをマージして ``resultMap`` を構築する + (スクリプトはこの ``resultMap`` に対して評価される)。 +#. ``scriptMap`` の各エントリを ``convertValue(scriptType, template, resultMap)`` で評価し、 + 結果を ``dataMap`` に格納する。マッピングはコード内にハードコードするのではなく、 + 管理者が「スクリプト」欄で定義する点が重要です。 +#. ``callback.store(paramMap, dataMap)`` を呼び出してドキュメントとしてインデックス登録する。 実装例 ====== @@ -81,102 +174,118 @@ AbstractDataStore シンプルなデータストア ---------------------- +外部 API からレコードを取得してインデックス登録する例です。 + .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // パラメーターの取得 - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // パラメーターの取得(DataStoreParams では getAsString を使用する) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // スクリプトの評価タイプ(既定は Groovy) + final String scriptType = getScriptType(paramMap); + // レコード間の待機時間(ミリ秒) + final long readInterval = getReadInterval(paramMap); try { - // データの取得 - List documents = fetchDocuments(apiUrl, apiKey); + // 外部システムからデータを取得 + final List> records = fetchRecords(apiUrl, apiKey); - // 各ドキュメントを処理 - for (Document doc : documents) { + for (final Map record : records) { + // 既定フィールドをコピーして dataMap を初期化 final Map dataMap = new HashMap<>(defaultDataMap); - // データのマッピング - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // パラメーターとソースレコードをマージした resultMap を構築 + // (スクリプトはこの resultMap に対して評価される) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // スクリプトの実行(マッピング) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // scriptMap の各エントリを評価してインデックスフィールドを生成 + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } // インデックスに登録 - callback.store(paramMap, resultMap); + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } +``fetchRecords()`` は外部システムからレコード一覧を取得する独自メソッドです。取得した +各レコード(``Map``)のフィールド名が、``scriptMap`` のスクリプトから +参照できる名前になります。``DataStoreException`` は ``org.codelibs.fess.exception`` +パッケージのクラスです。 + ページネーション対応 -------------------- -.. code-block:: java +大量のデータを扱う場合は、ページ単位で取得しながら処理します。1レコードあたりの処理 +(``resultMap`` の構築・``scriptMap`` の評価・``callback.store()`` の呼び出し)を +``processRecord()`` のようなメソッドに切り出しておくと、取得ロジックと分離できます。 - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +.. code-block:: java - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); + int offset = 0; + final int pageSize = 100; - if (documents.isEmpty()) { - break; - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + if (records.isEmpty()) { + break; + } - page++; + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); } + + offset += pageSize; } 認証の実装 ========== +外部システムの認証は、コネクター側で実装します。以下は一般的な HTTP クライアント +ライブラリを用いた実装例で、|Fess| が提供する API ではありません。利用するライブラリは +プラグインの依存関係として同梱してください。 + OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // トークンのリフレッシュ - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // リフレッシュトークンを用いてアクセストークンを取得する例 + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } @@ -186,8 +295,8 @@ APIキー認証 .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -198,53 +307,174 @@ APIキー認証 エラーハンドリング ================== +処理を中断すべき致命的なエラーでは ``DataStoreException`` をスローします。 + .. code-block:: java @Override protected void storeData(...) { try { // 処理 - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); + sleep(60000L); // リトライロジック - } catch (Exception e) { + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + ``fess-ds-example`` をはじめとする実際のコネクターでは、1レコードのエラーで + クロール全体を止めないよう、レコード単位で ``CrawlingAccessException`` を捕捉し、 + ``FailureUrlService`` にエラー URL を記録しています。また + ``DataStoreCrawlingException`` の中断フラグを用いて、クロール全体を中断するかどうかを + 制御しています。堅牢なコネクターを実装する場合は ``ExampleDataStore`` の実装を参照してください。 + テスト ====== ユニットテスト -------------- +|Fess| のプラグインは UTFlute の ``LastaDiTestCase`` を用いてテストします。テストは +JUnit 5(Jupiter)で記述します。``IndexUpdateCallback`` を、登録された ``dataMap`` を +収集する実装に差し替えることで、モックライブラリを使わずにマッピング結果を検証できます。 + .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { + + public ExampleDataStore dataStore; - private ExampleDataStore dataStore; + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } - @Before - public void setUp() { + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // モックを使用したテスト + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // 登録された dataMap を収集するテスト用コールバック + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // 何もしない + } + + public List> getDataMapList() { + return dataMapList; + } } } +.. note:: + + ``setUp`` は基底クラスで ``@BeforeEach`` が付与されているため、オーバーライド側に + ライフサイクルアノテーションを付け直す必要はありません。各テストメソッドには + ``@Test``(``org.junit.jupiter.api.Test``)を付与します。 + +ビルドとインストール +==================== + +pom.xml +------- + +プラグインは ``fess-parent`` を親 POM とする jar としてビルドします。``fess`` および +``opensearch`` への依存は、実行時に |Fess| 本体から提供されるため ``provided`` にします。 + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +テストには JUnit 5 と ``org.dbflute.utflute:utflute-lastaflute`` を利用します。 + +ビルド +------ + +.. code-block:: bash + + mvn clean package + +``target/`` ディレクトリに ``fess-ds-example-15.8.0.jar`` が生成されます。 + +インストール +------------ + +生成した JAR を |Fess| にインストールし、|Fess| を再起動します。インストール手順の詳細は +:doc:`../admin/plugin-guide` を参照してください。インストール後、管理画面の +「クローラー > データストア」から新規設定を作成し、「ハンドラー名」に ``getName()`` が返す名前 +(この例では ``ExampleDataStore``)を指定します。 + 設定例 ====== @@ -253,27 +483,41 @@ APIキー認証 パラメーター ------------ +「パラメーター」欄には、コネクターが ``paramMap`` から読み取るキーと値を記述します。 + :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 スクリプト ---------- +「スクリプト」欄には、``左辺=右辺`` の形式でマッピングを記述します。左辺がインデックス +フィールド名、右辺がソースレコードのフィールドを参照するスクリプト(既定では Groovy)です。 +以下は、ソースレコードが ``url`` / ``title`` / ``content`` / ``updated_at`` / +``content_type`` フィールドを持つ場合の例です。 + :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + 右辺で参照できるフィールド名は、コネクターが ``resultMap`` に格納する値 + (``paramMap`` の値とソースレコードのフィールド)によって決まります。CSV や JSON など + 既存コネクターでは ``data.*`` のような独自のプレフィックスが付く場合があるため、 + 各コネクターのドキュメントを参照してください。 参考情報 ======== - :doc:`plugin-architecture` - プラグインアーキテクチャ -- :doc:`../config/datastore/ds-overview` - データストアコネクタ概要 -- `GitHub: fess-ds-* `__ - 公開プラグインの例 +- :doc:`../admin/plugin-guide` - プラグインのインストール +- :doc:`../config/datastore/ds-overview` - データストアコネクター概要 +- `fess-ds-example `__ - データストアプラグインの実装テンプレート +- `GitHub: fess-ds-* `__ - 公開コネクターの例 diff --git a/ko/15.8/dev/datastore-plugin.rst b/ko/15.8/dev/datastore-plugin.rst index 311885801..37d406d6a 100644 --- a/ko/15.8/dev/datastore-plugin.rst +++ b/ko/15.8/dev/datastore-plugin.rst @@ -5,13 +5,24 @@ 개요 ==== -데이터스토어 플러그인을 개발하여 |Fess| 에 새로운 데이터 소스에서 -콘텐츠 취득 기능을 추가할 수 있습니다. +데이터스토어 플러그인을 개발하여 |Fess| 에 새로운 데이터 소스로부터 +콘텐츠 취득 기능을 추가할 수 있습니다. 데이터스토어는 데이터베이스·API·파일 등의 +외부 시스템에서 레코드를 취득하고, 관리 화면에서 설정한 매핑 스크립트에 따라 +인덱스 필드로 변환한 다음, |Fess| 의 인덱스에 등록합니다. + +CSV·JSON·데이터베이스·Git·각종 클라우드 서비스 등, 공개되어 있는 +커넥터(``fess-ds-*``)는 모두 이 구조로 구현되어 있습니다. 구현 템플릿으로 +`fess-ds-example `__ 가 공개되어 있으므로, +새로운 커넥터를 작성할 때는 이를 복사해서 시작하는 것이 간단합니다. 기본 구조 ======== -데이터스토어 플러그인은 ``AbstractDataStore`` 를 상속하여 구현합니다. +데이터스토어 플러그인은 다음 3가지 요소로 구성됩니다. + +- ``AbstractDataStore`` 를 상속한 클래스를 작성한다 +- ``getName()`` 과 ``storeData()`` 두 개의 메서드를 구현한다 +- ``fess_ds++.xml`` 에 컴포넌트로 등록한다 최소 구현 -------- @@ -24,27 +35,54 @@ import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // 핸들러 이름으로 사용된다. 클래스의 단순 이름을 반환하는 것이 관례 + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { // 데이터 취득 및 처리를 여기에 구현 } } +.. note:: + + ``getName()`` 과 ``storeData()`` 는 모두 ``protected`` 추상 메서드입니다. + ``DataConfig`` 의 패키지는 |Fess| 15.x 에서는 ``org.codelibs.fess.opensearch.config.exentity`` + 임에 유의하십시오(이전의 ``org.codelibs.fess.es.config.exentity`` 는 폐지되었습니다). + +컴포넌트 등록 +-------------------- + +작성한 데이터스토어를 |Fess| 가 인식하도록 하기 위해, ``src/main/resources/fess_ds++.xml`` 에 +컴포넌트를 등록합니다. + +.. code-block:: xml + + + + + + + + + +```` 에 의해 컴포넌트 생성 후 ``AbstractDataStore`` +가 가진 ``register()`` 메서드가 자동으로 호출되어 ``DataStoreFactory`` 에 자신이 등록됩니다. +이때 등록되는 이름이 ``getName()`` 의 반환값(위 예에서는 ``ExampleDataStore``)이며, +관리 화면의 데이터스토어 설정에서 선택하는 「핸들러 이름」이 됩니다. + AbstractDataStore ================= @@ -53,27 +91,82 @@ AbstractDataStore .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 * - 메서드 + - 구분 - 설명 * - ``getName()`` - - 데이터스토어 이름을 반환 (필수) + - 구현(필수) + - 데이터스토어의 핸들러 이름을 반환하는 추상 메서드. ``getClass().getSimpleName()`` 을 반환하는 것이 관례 * - ``storeData()`` - - 데이터 취득 및 인덱스 등록 수행 (필수) + - 구현(필수) + - 데이터의 취득·변환·인덱스 등록을 수행하는 추상 메서드 * - ``register()`` - - 플러그인 등록 - -파라미터 ------------- + - 상속(통상 변경 불필요) + - ``fess_ds++.xml`` 의 ``postConstruct`` 에서 자동으로 호출되어 ``DataStoreFactory`` 에 등록한다 + * - ``store()`` + - 상속(프레임워크 호출) + - 프레임워크가 호출하는 진입점. ``defaultDataMap`` 등을 준비하여 ``storeData()`` 를 호출한다 + * - ``convertValue()`` + - 상속(헬퍼) + - ``scriptMap`` 의 값(템플릿)을 스크립트 엔진으로 평가한다 + * - ``getScriptType()`` + - 상속(헬퍼) + - ``script_type`` 파라미터를 취득한다(기본값은 Groovy) + * - ``getReadInterval()`` + - 상속(헬퍼) + - ``readInterval`` 파라미터(밀리초)를 취득한다 + * - ``sleep()`` + - 상속(헬퍼) + - 지정한 밀리초만큼 대기한다(레코드 간 대기에 사용) + +storeData의 파라미터 +------------------------ ``storeData()`` 메서드에 전달되는 파라미터: -- ``dataConfig``: 데이터스토어 설정 -- ``callback``: 인덱스 업데이트용 콜백 -- ``paramMap``: 관리 화면에서 설정된 파라미터 -- ``scriptMap``: 스크립트 설정 -- ``defaultDataMap``: 기본 데이터 맵 +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - 파라미터 + - 타입 + - 설명 + * - ``dataConfig`` + - ``DataConfig`` + - 데이터스토어 설정(ID·핸들러 이름·파라미터·스크립트 등) + * - ``callback`` + - ``IndexUpdateCallback`` + - 생성한 문서를 인덱스에 등록하는 콜백 + * - ``paramMap`` + - ``DataStoreParams`` + - 관리 화면 「파라미터」란의 설정값. ``getAsString(key)`` / ``getAsString(key, default)`` / ``get(key)`` / ``asMap()`` / ``containsKey(key)`` 로 접근한다 + * - ``scriptMap`` + - ``Map`` + - 관리 화면 「스크립트」란의 설정. 키가 인덱스 필드 이름, 값이 평가 대상 스크립트 템플릿 + * - ``defaultDataMap`` + - ``Map`` + - 각 문서의 기본 필드 값(설정 ID·boost·role·mimetype·가상 호스트 등). 프레임워크가 준비한다 + +.. warning:: + + ``paramMap`` 의 타입은 ``Map`` 이 아니라 ``DataStoreParams`` 입니다. + ``DataStoreParams`` 는 ``Map`` 을 구현하지 않으므로, 값을 가져올 때는 ``get()`` 이 아니라 + 문자열을 반환하는 ``getAsString()`` 을 사용하십시오. + +데이터 처리 흐름 +---------------- + +``storeData()`` 구현은 다음 흐름으로 데이터를 처리합니다. + +#. 외부 시스템에서 소스 레코드를 취득한다. +#. ``paramMap.asMap()`` 에 소스 레코드의 필드를 병합하여 ``resultMap`` 을 구축한다 + (스크립트는 이 ``resultMap`` 에 대해 평가된다). +#. ``scriptMap`` 의 각 엔트리를 ``convertValue(scriptType, template, resultMap)`` 로 평가하여 + 결과를 ``dataMap`` 에 저장한다. 매핑을 코드 안에 하드코딩하는 것이 아니라 + 관리자가 「스크립트」란에서 정의한다는 점이 중요합니다. +#. ``callback.store(paramMap, dataMap)`` 을 호출하여 문서로 인덱스에 등록한다. 구현 예 ====== @@ -81,102 +174,118 @@ AbstractDataStore 심플한 데이터스토어 ---------------------- +외부 API 에서 레코드를 취득하여 인덱스에 등록하는 예입니다. + .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // 파라미터 취득 - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // 파라미터 취득(DataStoreParams 에서는 getAsString 을 사용한다) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // 스크립트 평가 타입(기본값은 Groovy) + final String scriptType = getScriptType(paramMap); + // 레코드 간 대기 시간(밀리초) + final long readInterval = getReadInterval(paramMap); try { - // 데이터 취득 - List documents = fetchDocuments(apiUrl, apiKey); + // 외부 시스템에서 데이터 취득 + final List> records = fetchRecords(apiUrl, apiKey); - // 각 문서 처리 - for (Document doc : documents) { + for (final Map record : records) { + // 기본 필드를 복사하여 dataMap 초기화 final Map dataMap = new HashMap<>(defaultDataMap); - // 데이터 매핑 - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // 파라미터와 소스 레코드를 병합한 resultMap 구축 + // (스크립트는 이 resultMap 에 대해 평가된다) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // 스크립트 실행 (매핑) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // scriptMap 의 각 엔트리를 평가하여 인덱스 필드 생성 + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } // 인덱스에 등록 - callback.store(paramMap, resultMap); + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } +``fetchRecords()`` 는 외부 시스템에서 레코드 목록을 취득하는 자체 메서드입니다. 취득한 +각 레코드(``Map``)의 필드 이름이, ``scriptMap`` 의 스크립트에서 +참조할 수 있는 이름이 됩니다. ``DataStoreException`` 은 ``org.codelibs.fess.exception`` +패키지의 클래스입니다. + 페이지네이션 대응 -------------------- -.. code-block:: java +대량의 데이터를 다루는 경우에는 페이지 단위로 취득하면서 처리합니다. 레코드 1건당 처리 +(``resultMap`` 구축·``scriptMap`` 평가·``callback.store()`` 호출)를 +``processRecord()`` 와 같은 메서드로 분리해두면 취득 로직과 분리할 수 있습니다. - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +.. code-block:: java - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); + int offset = 0; + final int pageSize = 100; - if (documents.isEmpty()) { - break; - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + if (records.isEmpty()) { + break; + } - page++; + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); } + + offset += pageSize; } 인증 구현 ========== +외부 시스템의 인증은 커넥터 측에서 구현합니다. 아래는 일반적인 HTTP 클라이언트 +라이브러리를 사용한 구현 예시로, |Fess| 가 제공하는 API 는 아닙니다. 사용할 라이브러리는 +플러그인의 의존 관계로 함께 포함시켜 주십시오. + OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // 토큰 리프레시 - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // 리프레시 토큰을 이용해 액세스 토큰을 취득하는 예 + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } @@ -186,8 +295,8 @@ API 키 인증 .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -198,53 +307,174 @@ API 키 인증 오류 처리 ================== +처리를 중단해야 하는 치명적인 오류에서는 ``DataStoreException`` 을 던집니다. + .. code-block:: java @Override protected void storeData(...) { try { // 처리 - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); + sleep(60000L); // 재시도 로직 - } catch (Exception e) { + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + ``fess-ds-example`` 을 비롯한 실제 커넥터에서는 레코드 1건의 오류로 + 크롤링 전체가 중단되지 않도록, 레코드 단위로 ``CrawlingAccessException`` 을 포착하여 + ``FailureUrlService`` 에 오류 URL 을 기록합니다. 또한 + ``DataStoreCrawlingException`` 의 중단 플래그를 사용하여, 크롤링 전체를 중단할지 여부를 + 제어합니다. 견고한 커넥터를 구현하려는 경우에는 ``ExampleDataStore`` 의 구현을 참조하십시오. + 테스트 ====== 유닛 테스트 -------------- +|Fess| 의 플러그인은 UTFlute 의 ``LastaDiTestCase`` 를 사용하여 테스트합니다. 테스트는 +JUnit 5(Jupiter)로 작성합니다. ``IndexUpdateCallback`` 을, 등록된 ``dataMap`` 을 +수집하는 구현으로 교체함으로써, 목(mock) 라이브러리를 사용하지 않고도 매핑 결과를 검증할 수 있습니다. + .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { + + public ExampleDataStore dataStore; - private ExampleDataStore dataStore; + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } - @Before - public void setUp() { + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // 목을 사용한 테스트 + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // 등록된 dataMap 을 수집하는 테스트용 콜백 + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // 아무것도 하지 않음 + } + + public List> getDataMapList() { + return dataMapList; + } } } +.. note:: + + ``setUp`` 은 기반 클래스에서 ``@BeforeEach`` 가 부여되어 있으므로, 오버라이드하는 쪽에 + 라이프사이클 애너테이션을 다시 붙일 필요가 없습니다. 각 테스트 메서드에는 + ``@Test``(``org.junit.jupiter.api.Test``)를 부여합니다. + +빌드와 설치 +==================== + +pom.xml +------- + +플러그인은 ``fess-parent`` 를 부모 POM 으로 하는 jar 로 빌드합니다. ``fess`` 및 +``opensearch`` 에 대한 의존성은 실행 시 |Fess| 본체에서 제공되므로 ``provided`` 로 설정합니다. + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +테스트에는 JUnit 5 와 ``org.dbflute.utflute:utflute-lastaflute`` 를 사용합니다. + +빌드 +------ + +.. code-block:: bash + + mvn clean package + +``target/`` 디렉터리에 ``fess-ds-example-15.8.0.jar`` 가 생성됩니다. + +설치 +------------ + +생성한 JAR 를 |Fess| 에 설치하고 |Fess| 를 재시작합니다. 설치 절차의 자세한 내용은 +:doc:`../admin/plugin-guide` 를 참조하십시오. 설치 후 관리 화면의 +「크롤러 > 데이터스토어」에서 신규 설정을 작성하고, 「핸들러 이름」에 ``getName()`` 이 반환하는 이름 +(이 예에서는 ``ExampleDataStore``)을 지정합니다. + 설정 예 ====== @@ -253,27 +483,41 @@ API 키 인증 파라미터 ------------ +「파라미터」란에는 커넥터가 ``paramMap`` 에서 읽어들이는 키와 값을 기술합니다. + :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 스크립트 ---------- +「스크립트」란에는 ``좌변=우변`` 형식으로 매핑을 기술합니다. 좌변이 인덱스 +필드 이름, 우변이 소스 레코드의 필드를 참조하는 스크립트(기본값은 Groovy)입니다. +아래는 소스 레코드가 ``url`` / ``title`` / ``content`` / ``updated_at`` / +``content_type`` 필드를 가지고 있는 경우의 예입니다. + :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + 우변에서 참조할 수 있는 필드 이름은 커넥터가 ``resultMap`` 에 저장하는 값 + (``paramMap`` 의 값과 소스 레코드의 필드)에 따라 결정됩니다. CSV 나 JSON 등 + 기존 커넥터에서는 ``data.*`` 와 같은 독자적인 프리픽스가 붙는 경우가 있으므로, + 각 커넥터의 문서를 참조하십시오. 참고 정보 ======== - :doc:`plugin-architecture` - 플러그인 아키텍처 +- :doc:`../admin/plugin-guide` - 플러그인 설치 - :doc:`../config/datastore/ds-overview` - 데이터스토어 커넥터 개요 -- `GitHub: fess-ds-* `__ - 공개 플러그인 예시 +- `fess-ds-example `__ - 데이터스토어 플러그인 구현 템플릿 +- `GitHub: fess-ds-* `__ - 공개 커넥터 예시 diff --git a/zh-cn/15.8/dev/datastore-plugin.rst b/zh-cn/15.8/dev/datastore-plugin.rst index cc1a9adca..5d8f13255 100644 --- a/zh-cn/15.8/dev/datastore-plugin.rst +++ b/zh-cn/15.8/dev/datastore-plugin.rst @@ -5,12 +5,23 @@ 概述 ==== -通过开发数据存储插件,可以为 |Fess| 添加从新数据源获取内容的功能。 +通过开发数据存储插件,可以为 |Fess| 添加从新数据源获取内容的功能。数据存储会从数据库、 +API、文件等外部系统中获取记录,并按照在管理界面中设置的映射脚本,将其转换为索引字段后, +注册到 |Fess| 的索引中。 + +CSV、JSON、数据库、Git、各类云服务等公开的连接器(``fess-ds-*``)都是基于这一机制实现 +的。作为实现模板,官方公开了 +`fess-ds-example `__ ,创建新连接器时, +复制该模板作为起点会比较简单。 基本结构 ======== -数据存储插件通过继承 ``AbstractDataStore`` 来实现。 +数据存储插件由以下三个要素构成: + +- 创建继承 ``AbstractDataStore`` 的类 +- 实现 ``getName()`` 和 ``storeData()`` 两个方法 +- 在 ``fess_ds++.xml`` 中注册为组件 最小实现 -------- @@ -23,159 +34,255 @@ import org.codelibs.fess.ds.AbstractDataStore; import org.codelibs.fess.ds.callback.IndexUpdateCallback; + import org.codelibs.fess.entity.DataStoreParams; import org.codelibs.fess.opensearch.config.exentity.DataConfig; public class ExampleDataStore extends AbstractDataStore { @Override - public String getName() { - return "Example"; + protected String getName() { + // 用作处理器名。按照惯例返回类的简单名称 + return this.getClass().getSimpleName(); } @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // 在此实现数据获取和处理 + // 在此实现数据的获取与处理 } } +.. note:: + + ``getName()`` 和 ``storeData()`` 都是 ``protected`` 的抽象方法。请注意,在 |Fess| + 15.x 中,``DataConfig`` 所在的包是 ``org.codelibs.fess.opensearch.config.exentity`` + (此前的 ``org.codelibs.fess.es.config.exentity`` 已被废弃)。 + +组件注册 +-------------------- + +为了让 |Fess| 识别所创建的数据存储,需要在 ``src/main/resources/fess_ds++.xml`` 中 +注册组件。 + +.. code-block:: xml + + + + + + + + + +通过 ````,组件生成后会自动调用 ``AbstractDataStore`` +所具有的 ``register()`` 方法,将自身注册到 ``DataStoreFactory`` 中。此时注册的名称即为 +``getName()`` 的返回值(上例中为 ``ExampleDataStore``),也就是在管理界面的数据存储设置 +中选择的"处理器名"。 + AbstractDataStore ================= 主要方法 --------- +------------ .. list-table:: :header-rows: 1 - :widths: 30 70 + :widths: 35 20 45 * - 方法 + - 类别 - 说明 * - ``getName()`` - - 返回数据存储名称(必需) + - 实现(必需) + - 返回数据存储处理器名的抽象方法。按照惯例返回 ``getClass().getSimpleName()`` * - ``storeData()`` - - 执行数据获取和索引注册(必需) + - 实现(必需) + - 执行数据获取、转换及索引注册的抽象方法 * - ``register()`` - - 注册插件 + - 继承(通常无需修改) + - 由 ``fess_ds++.xml`` 的 ``postConstruct`` 自动调用,注册到 ``DataStoreFactory`` + * - ``store()`` + - 继承(框架调用) + - 框架调用的入口。准备好 ``defaultDataMap`` 等后调用 ``storeData()`` + * - ``convertValue()`` + - 继承(辅助方法) + - 通过脚本引擎对 ``scriptMap`` 的值(模板)进行求值 + * - ``getScriptType()`` + - 继承(辅助方法) + - 获取 ``script_type`` 参数(默认为 Groovy) + * - ``getReadInterval()`` + - 继承(辅助方法) + - 获取 ``readInterval`` 参数(毫秒) + * - ``sleep()`` + - 继承(辅助方法) + - 休眠指定的毫秒数(用于记录间的等待) + +storeData 的参数 +------------------------ + +传递给 ``storeData()`` 方法的参数: -参数 ----- - -传递给 ``storeData()`` 方法的参数: +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 -- ``dataConfig``: 数据存储配置 -- ``callback``: 索引更新回调 -- ``paramMap``: 管理界面设置的参数 -- ``scriptMap``: 脚本设置 -- ``defaultDataMap``: 默认数据映射 + * - 参数 + - 类型 + - 说明 + * - ``dataConfig`` + - ``DataConfig`` + - 数据存储设置(ID、处理器名、参数、脚本等) + * - ``callback`` + - ``IndexUpdateCallback`` + - 将生成的文档注册到索引的回调 + * - ``paramMap`` + - ``DataStoreParams`` + - 管理界面"参数"栏中设置的值。可通过 ``getAsString(key)`` / ``getAsString(key, default)`` / ``get(key)`` / ``asMap()`` / ``containsKey(key)`` 进行访问 + * - ``scriptMap`` + - ``Map`` + - 管理界面"脚本"栏中的设置。键为索引字段名,值为待求值的脚本模板 + * - ``defaultDataMap`` + - ``Map`` + - 各文档的默认字段值(配置ID、boost、role、mimetype、虚拟主机等)。由框架准备 + +.. warning:: + + ``paramMap`` 的类型不是 ``Map``,而是 ``DataStoreParams``。由于 + ``DataStoreParams`` 并未实现 ``Map`` 接口,获取值时请使用返回字符串的 + ``getAsString()``,而不是 ``get()``。 + +数据处理流程 +---------------- + +``storeData()`` 的实现按以下流程处理数据: + +#. 从外部系统获取源记录。 +#. 将源记录的字段合并到 ``paramMap.asMap()`` 中,构建 ``resultMap`` + (脚本将针对该 ``resultMap`` 进行求值)。 +#. 使用 ``convertValue(scriptType, template, resultMap)`` 对 ``scriptMap`` 中的每个 + 条目求值,并将结果存入 ``dataMap``。需要注意的是,映射关系并非硬编码在代码中,而是由 + 管理员在"脚本"栏中定义。 +#. 调用 ``callback.store(paramMap, dataMap)``,将其作为文档注册到索引中。 实现示例 ======== 简单的数据存储 --------------- +---------------------- + +以下是从外部 API 获取记录并注册到索引的示例。 .. code-block:: java @Override - protected void storeData( - final DataConfig dataConfig, - final IndexUpdateCallback callback, - final Map paramMap, - final Map scriptMap, + protected void storeData(final DataConfig dataConfig, final IndexUpdateCallback callback, + final DataStoreParams paramMap, final Map scriptMap, final Map defaultDataMap) { - // 获取参数 - final String apiUrl = paramMap.get("api.url"); - final String apiKey = paramMap.get("api.key"); + // 获取参数(DataStoreParams 需使用 getAsString) + final String apiUrl = paramMap.getAsString("api.url"); + final String apiKey = paramMap.getAsString("api.key"); + + // 脚本的求值类型(默认为 Groovy) + final String scriptType = getScriptType(paramMap); + // 记录之间的等待时间(毫秒) + final long readInterval = getReadInterval(paramMap); try { - // 获取数据 - List documents = fetchDocuments(apiUrl, apiKey); + // 从外部系统获取数据 + final List> records = fetchRecords(apiUrl, apiKey); - // 处理每个文档 - for (Document doc : documents) { + for (final Map record : records) { + // 复制默认字段以初始化 dataMap final Map dataMap = new HashMap<>(defaultDataMap); - // 数据映射 - dataMap.put("url", doc.getUrl()); - dataMap.put("title", doc.getTitle()); - dataMap.put("content", doc.getContent()); - dataMap.put("lastModified", doc.getUpdatedAt()); + // 构建合并了参数与源记录的 resultMap + // (脚本将针对此 resultMap 进行求值) + final Map resultMap = new LinkedHashMap<>(paramMap.asMap()); + resultMap.putAll(record); - // 执行脚本(映射) - final Map resultMap = new HashMap<>(); - for (Map.Entry entry : scriptMap.entrySet()) { - Object value = convertValue(entry.getValue(), dataMap); + // 对 scriptMap 中的每个条目求值,生成索引字段 + for (final Map.Entry entry : scriptMap.entrySet()) { + final Object value = convertValue(scriptType, entry.getValue(), resultMap); if (value != null) { - resultMap.put(entry.getKey(), value); + dataMap.put(entry.getKey(), value); } } // 注册到索引 - callback.store(paramMap, resultMap); + callback.store(paramMap, dataMap); + + if (readInterval > 0) { + sleep(readInterval); + } } - } catch (Exception e) { - logger.error("Failed to crawl data", e); + } catch (final Exception e) { + throw new DataStoreException("Failed to crawl data from " + apiUrl, e); } } +``fetchRecords()`` 是从外部系统获取记录列表的自定义方法。获取到的每条记录 +(``Map``)的字段名,即为可在 ``scriptMap`` 的脚本中引用的名称。 +``DataStoreException`` 是 ``org.codelibs.fess.exception`` 包中的类。 + 分页支持 --------- +-------------------- -.. code-block:: java +在处理大量数据时,需要按页获取并处理数据。将每条记录的处理逻辑(构建 ``resultMap``、 +对 ``scriptMap`` 求值、调用 ``callback.store()``)拆分到类似 ``processRecord()`` 的 +方法中,可以将其与获取逻辑分离。 - @Override - protected void storeData(...) { - int page = 0; - int pageSize = 100; +.. code-block:: java - while (true) { - List documents = fetchDocuments(apiUrl, apiKey, page, pageSize); + int offset = 0; + final int pageSize = 100; - if (documents.isEmpty()) { - break; - } + while (true) { + final List> records = fetchRecords(apiUrl, apiKey, offset, pageSize); - for (Document doc : documents) { - processDocument(doc, callback, paramMap, scriptMap, defaultDataMap); - } + if (records.isEmpty()) { + break; + } - page++; + for (final Map record : records) { + processRecord(record, callback, paramMap, scriptMap, defaultDataMap); } + + offset += pageSize; } 认证实现 -======== +========== + +外部系统的认证由连接器一侧负责实现。以下是使用常见 HTTP 客户端库的实现示例,并非 +|Fess| 提供的 API。请将所使用的库作为插件的依赖项一并打包。 OAuth 2.0 --------- .. code-block:: java - protected String getAccessToken(String clientId, String clientSecret, String refreshToken) { - // 刷新令牌 - OkHttpClient client = new OkHttpClient(); + protected String getAccessToken(final String clientId, final String clientSecret, final String refreshToken) { + // 使用刷新令牌获取访问令牌的示例 + final OkHttpClient client = new OkHttpClient(); - RequestBody body = new FormBody.Builder() + final RequestBody body = new FormBody.Builder() .add("grant_type", "refresh_token") .add("client_id", clientId) .add("client_secret", clientSecret) .add("refresh_token", refreshToken) .build(); - Request request = new Request.Builder() + final Request request = new Request.Builder() .url("https://oauth.example.com/token") .post(body) .build(); try (Response response = client.newCall(request).execute()) { - JsonNode json = objectMapper.readTree(response.body().string()); + final JsonNode json = objectMapper.readTree(response.body().string()); return json.get("access_token").asText(); } } @@ -185,8 +292,8 @@ API密钥认证 .. code-block:: java - protected Response callApi(String url, String apiKey) { - Request request = new Request.Builder() + protected Response callApi(final String url, final String apiKey) { + final Request request = new Request.Builder() .url(url) .addHeader("Authorization", "Bearer " + apiKey) .build(); @@ -195,7 +302,9 @@ API密钥认证 } 错误处理 -======== +================== + +对于应中断处理的致命错误,请抛出 ``DataStoreException``。 .. code-block:: java @@ -203,77 +312,205 @@ API密钥认证 protected void storeData(...) { try { // 处理 - } catch (AuthenticationException e) { + } catch (final AuthenticationException e) { logger.error("Authentication failed. Check your credentials.", e); throw new DataStoreException("Authentication failed", e); - } catch (RateLimitException e) { + } catch (final RateLimitException e) { logger.warn("Rate limit exceeded. Waiting..."); - Thread.sleep(60000); + sleep(60000L); // 重试逻辑 - } catch (Exception e) { + } catch (final Exception e) { logger.error("Unexpected error occurred", e); throw new DataStoreException("Failed to crawl data", e); } } +.. note:: + + 在以 ``fess-ds-example`` 为代表的实际连接器中,为了避免单条记录的错误导致整个爬取 + 中断,会以记录为单位捕获 ``CrawlingAccessException``,并将出错的 URL 记录到 + ``FailureUrlService`` 中。此外,还会利用 ``DataStoreCrawlingException`` 的中断 + 标志,来控制是否中断整个爬取过程。如需实现健壮的连接器,请参考 ``ExampleDataStore`` + 的实现。 + 测试 -==== +====== 单元测试 --------- +-------------- + +|Fess| 的插件使用 UTFlute 的 ``LastaDiTestCase`` 进行测试。测试使用 JUnit 5 +(Jupiter)编写。通过将 ``IndexUpdateCallback`` 替换为收集已注册 ``dataMap`` 的实现, +无需使用模拟(mock)库即可验证映射结果。 .. code-block:: java - public class ExampleDataStoreTest { + public class ExampleDataStoreTest extends LastaDiTestCase { - private ExampleDataStore dataStore; + public ExampleDataStore dataStore; - @Before - public void setUp() { + @Override + protected String prepareConfigFile() { + return "test_app.xml"; + } + + @Override + public void setUp(final TestInfo testInfo) throws Exception { + super.setUp(testInfo); dataStore = new ExampleDataStore(); } @Test - public void testGetName() { - assertEquals("Example", dataStore.getName()); + public void test_getName() { + assertEquals("ExampleDataStore", dataStore.getName()); } @Test - public void testFetchDocuments() { - // 使用mock进行测试 + public void test_storeData() { + final TestIndexUpdateCallback callback = new TestIndexUpdateCallback(); + final DataStoreParams paramMap = new DataStoreParams(); + + final Map scriptMap = new HashMap<>(); + scriptMap.put("title", "title"); + + dataStore.storeData(new DataConfig(), callback, paramMap, scriptMap, new HashMap<>()); + + assertFalse(callback.getDataMapList().isEmpty()); + } + + // 用于收集已注册 dataMap 的测试用回调 + private static class TestIndexUpdateCallback implements IndexUpdateCallback { + private final List> dataMapList = new ArrayList<>(); + + @Override + public void store(final DataStoreParams paramMap, final Map dataMap) { + dataMapList.add(new HashMap<>(dataMap)); + } + + @Override + public long getExecuteTime() { + return 0; + } + + @Override + public long getDocumentSize() { + return dataMapList.size(); + } + + @Override + public void commit() { + // 不执行任何操作 + } + + public List> getDataMapList() { + return dataMapList; + } } } +.. note:: + + 由于基类中的 ``setUp`` 已附加了 ``@BeforeEach``,因此重写时无需在其上重新附加生命 + 周期注解。请为每个测试方法附加 ``@Test``(``org.junit.jupiter.api.Test``)。 + +构建与安装 +==================== + +pom.xml +------- + +插件以 ``fess-parent`` 作为父 POM,构建为 jar。对 ``fess`` 和 ``opensearch`` 的依赖, +由于在运行时由 |Fess| 本体提供,因此设置为 ``provided``。 + +.. code-block:: xml + + + 4.0.0 + org.codelibs.fess + fess-ds-example + 15.8.0 + jar + + + org.codelibs.fess + fess-parent + 15.8.0 + + + + + + org.codelibs.fess + fess + provided + + + org.opensearch + opensearch + provided + + + + +测试使用 JUnit 5 和 ``org.dbflute.utflute:utflute-lastaflute``。 + +构建 +------ + +.. code-block:: bash + + mvn clean package + +会在 ``target/`` 目录下生成 ``fess-ds-example-15.8.0.jar``。 + +安装 +------------ + +将生成的 JAR 安装到 |Fess| 中,并重启 |Fess|。安装步骤的详细信息请参考 +:doc:`../admin/plugin-guide`。安装后,从管理界面的"爬虫 > 数据存储"创建新设置,并在 +"处理器名"中指定 ``getName()`` 返回的名称(本例中为 ``ExampleDataStore``)。 + 配置示例 ======== -管理界面的配置示例: +管理界面中的配置示例: 参数 ----- +------------ + +"参数"栏中填写连接器从 ``paramMap`` 读取的键和值。 :: api.url=https://api.example.com/v1 api.key=your_api_key - max.items=1000 - include.folders=folder1,folder2 脚本 ----- +---------- + +"脚本"栏中以 ``左边=右边`` 的形式描述映射关系。左边为索引字段名,右边为引用源记录字段 +的脚本(默认使用 Groovy)。以下是源记录具有 ``url`` / ``title`` / ``content`` / +``updated_at`` / ``content_type`` 字段时的示例。 :: - url=data.url - title=data.name - content=data.content - lastModified=data.updated_at - mimetype=data.content_type + url=url + title=title + content=content + last_modified=updated_at + mimetype=content_type + +.. note:: + + 右边可引用的字段名,取决于连接器存入 ``resultMap`` 中的值(``paramMap`` 的值以及 + 源记录的字段)。在 CSV、JSON 等现有连接器中,可能会带有类似 ``data.*`` 的专属前缀, + 请参考各连接器的文档。 参考信息 ======== - :doc:`plugin-architecture` - 插件架构 +- :doc:`../admin/plugin-guide` - 插件安装 - :doc:`../config/datastore/ds-overview` - 数据存储连接器概述 -- `GitHub: fess-ds-* `__ - 公开插件示例 - +- `fess-ds-example `__ - 数据存储插件的实现模板 +- `GitHub: fess-ds-* `__ - 公开连接器示例