View Javadoc
1   package com.acumenvelocity.ath.solr;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.List;
6   
7   import org.apache.solr.client.solrj.SolrClient;
8   import org.apache.solr.common.SolrInputDocument;
9   
10  import com.acumenvelocity.ath.common.Log;
11  import com.acumenvelocity.ath.common.OkapiUtil;
12  import com.acumenvelocity.ath.steps.BaseMergerStep;
13  
14  import net.sf.okapi.common.Event;
15  import net.sf.okapi.common.LocaleId;
16  import net.sf.okapi.common.pipeline.BasePipelineStep;
17  import net.sf.okapi.common.resource.ISegments;
18  import net.sf.okapi.common.resource.ITextUnit;
19  import net.sf.okapi.common.resource.Segment;
20  import net.sf.okapi.common.resource.TextContainer;
21  
22  /**
23   * Abstract base class for indexing text units into a Solr instance using batched operations.
24   * Subclasses provide custom field population logic for index documents.
25   */
26  public abstract class SolrIndexWriterStep extends BasePipelineStep {
27  
28    private SolrClient solrClient;
29    private String coreName;
30    private boolean autoCommit;
31    private int tuBatchSize;
32    private List<ITextUnit> tuBatch;
33    private boolean requireTarget = true;
34  
35    protected abstract SolrInputDocument getSolrDocument(SolrClient solrClient, String coreName,
36        String sourceWithCodes);
37  
38    /**
39     * Implement this method in subclasses to define how index document fields are set based on the
40     * provided text unit and segments.
41     * 
42     * @param doc    The index document to fill with data.
43     * @param tu     The text unit being processed.
44     * @param sseg   The source segment.
45     * @param srcLoc The source locale.
46     * @param tseg   The corresponding target segment (may be null).
47     * @param trgLoc The target locale.
48     * @throws Exception If an error occurs during field population.
49     */
50    protected abstract void populateSolrDocument(SolrInputDocument doc, ITextUnit tu,
51        Segment sseg, Segment tseg, LocaleId srcLoc, LocaleId trgLoc) throws Exception;
52  
53    /**
54     * Constructs the step with batch capacity, index identifier, and auto-commit option.
55     * 
56     * @param tuBatchSize Maximum number of TUs per TU batch.
57     * @param coreName    The identifier for the Solr index.
58     * @param autoCommit  Whether to commit changes automatically after each batch.
59     */
60    public SolrIndexWriterStep(SolrClient solrClient, int tuBatchSize, String coreName,
61        boolean autoCommit, boolean requireTarget) {
62  
63      this.solrClient = solrClient;
64      this.tuBatch = new ArrayList<>();
65      this.tuBatchSize = tuBatchSize;
66      this.coreName = coreName;
67      this.autoCommit = autoCommit;
68      this.requireTarget = requireTarget;
69    }
70  
71    @Override
72    public String getName() {
73      return "Solr Indexing Processor";
74    }
75  
76    @Override
77    public String getDescription() {
78      return "Handles batch indexing of text units into a Solr collection.";
79    }
80  
81    @Override
82    protected Event handleStartBatch(Event event) {
83      tuBatch.clear();
84      return event;
85    }
86  
87    @Override
88    protected Event handleTextUnit(Event event) {
89      ITextUnit tu = event.getTextUnit();
90  
91      if (tu != null) {
92        tuBatch.add(tu);
93  
94        if (tuBatch.size() >= tuBatchSize) {
95          processTuBatch();
96        }
97      }
98      return event;
99    }
100 
101   @Override
102   protected Event handleEndBatch(Event event) {
103     if (!tuBatch.isEmpty()) {
104       processTuBatch();
105     }
106 
107     return event;
108   }
109 
110   /**
111    * Executes the indexing operation for the current pending batch.
112    */
113   private void processTuBatch() {
114     if (tuBatch.isEmpty()) {
115       Log.warn(SolrIndexWriterStep.class, "Attempted to index an empty batch");
116       return;
117     }
118 
119     if (solrClient == null) {
120       throw new IllegalStateException("Index client must be configured prior to use.");
121     }
122 
123     try {
124       Collection<SolrInputDocument> indexDocs = generateIndexDocuments(tuBatch);
125 
126       if (indexDocs.size() < tuBatch.size()) {
127         Log.warn(SolrIndexWriterStep.class, "Generated {} index documents from {} text units",
128             indexDocs.size(), tuBatch.size());
129       }
130 
131       Log.debug(SolrIndexWriterStep.class, "Indexing {} documents into Solr", indexDocs.size());
132 
133       if (!indexDocs.isEmpty()) {
134         solrClient.add(coreName, indexDocs);
135 
136         if (autoCommit) {
137           solrClient.commit(coreName);
138         }
139       }
140 
141       Log.debug(SolrIndexWriterStep.class, "Completed indexing of {} documents", indexDocs.size());
142 
143     } catch (Exception e) {
144       throw new RuntimeException("Indexing operation failed", e);
145 
146     } finally {
147       tuBatch.clear();
148     }
149   }
150 
151   /**
152    * Transforms a list of text units into a collection of index documents.
153    * 
154    * @param tus The text units to transform.
155    * @return Collection of prepared index documents.
156    */
157   private Collection<SolrInputDocument> generateIndexDocuments(List<ITextUnit> tus) {
158     Collection<SolrInputDocument> docs = new ArrayList<>();
159 
160     for (ITextUnit tu : tus) {
161       if (tu == null) {
162         continue;
163       }
164 
165       // XXX Keep in sync with BaseMergerStep
166       if (!BaseMergerStep.checkTu(tu)) {
167         continue;
168       }
169 
170       Collection<LocaleId> trgLocs = tu.getTargetLocales();
171 
172       if (trgLocs.isEmpty() && requireTarget) {
173         Log.debug(SolrIndexWriterStep.class,
174             "Omitting text unit without output languages since target content is required");
175         continue;
176       }
177 
178       if (trgLocs.isEmpty()) {
179         processTuSegments(tu, null, docs);
180 
181       } else {
182         for (LocaleId lang : trgLocs) {
183           TextContainer target = tu.getTarget(lang);
184 
185           if (requireTarget && (target == null || !target.hasText(false))) {
186             Log.debug(SolrIndexWriterStep.class,
187                 "Omitting text unit for language {} due to missing content", lang);
188 
189             continue;
190           }
191 
192           processTuSegments(tu, lang, docs);
193         }
194       }
195     }
196 
197     return docs;
198   }
199 
200   /**
201    * Handles segment processing for a given text unit and optional output language.
202    * 
203    * @param tu     The text unit to process.
204    * @param trgLoc The output language (may be null).
205    * @param docs   The collection to add generated documents to.
206    */
207   private void processTuSegments(ITextUnit tu, LocaleId trgLoc,
208       Collection<SolrInputDocument> docs) {
209 
210     TextContainer source = tu.getSource();
211     TextContainer target = OkapiUtil.safeGetTarget(tu, trgLoc);
212 
213     ISegments sourceSegments = source.getSegments();
214 
215     for (Segment sseg : sourceSegments) {
216       Segment tseg = null;
217 
218       if (target != null) {
219         tseg = target.getSegments().get(sseg.getId());
220 
221         if (requireTarget && tseg == null) {
222           Log.debug(SolrIndexWriterStep.class, "Omitting segment {} due to missing output segment",
223               sseg.getId());
224 
225           continue;
226         }
227       }
228 
229       if (!requireTarget && tseg == null) {
230         Log.debug(SolrIndexWriterStep.class, "Proceeding with segment {} without output segment",
231             sseg.getId());
232       }
233 
234       // sseg.toString() => sseg.text.toText(), codes included
235       SolrInputDocument doc = getSolrDocument(solrClient, coreName, sseg.toString());
236 
237       try {
238         LocaleId effectiveOutputLang = (trgLoc != null) ? trgLoc : getTargetLocale();
239         populateSolrDocument(doc, tu, sseg, tseg, getSourceLocale(), effectiveOutputLang);
240 
241       } catch (Exception e) {
242         Log.warn(SolrIndexWriterStep.class, "Unable to populate index document for segment {}",
243             sseg.getId(), e);
244 
245         continue;
246       }
247 
248       docs.add(doc);
249     }
250   }
251 
252   public SolrClient getSolrClient() {
253     return solrClient;
254   }
255 
256   public String getCoreName() {
257     return coreName;
258   }
259 
260   public boolean isAutoCommit() {
261     return autoCommit;
262   }
263 
264   public int getTuBatchSize() {
265     return tuBatchSize;
266   }
267 
268   public List<ITextUnit> getTuBatch() {
269     return tuBatch;
270   }
271 
272   public boolean isRequireTarget() {
273     return requireTarget;
274   }
275 
276 }