~slub.team/goobi-indexserver/3.x

« back to all changes in this revision

Viewing changes to solr/core/src/java/org/apache/solr/handler/SpellCheckerRequestHandler.java

  • Committer: Sebastian Meyer
  • Date: 2012-08-03 09:12:40 UTC
  • Revision ID: sebastian.meyer@slub-dresden.de-20120803091240-x6861b0vabq1xror
Remove Lucene and Solr source code and add patches instead
Fix Bug #985487: Auto-suggestion for the search interface

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/**
2
 
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 
 * contributor license agreements.  See the NOTICE file distributed with
4
 
 * this work for additional information regarding copyright ownership.
5
 
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 
 * (the "License"); you may not use this file except in compliance with
7
 
 * the License.  You may obtain a copy of the License at
8
 
 *
9
 
 *     http://www.apache.org/licenses/LICENSE-2.0
10
 
 *
11
 
 * Unless required by applicable law or agreed to in writing, software
12
 
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 
 * See the License for the specific language governing permissions and
15
 
 * limitations under the License.
16
 
 */
17
 
 
18
 
package org.apache.solr.handler;
19
 
 
20
 
import org.apache.lucene.index.IndexReader;
21
 
import org.apache.lucene.index.IndexWriterConfig;
22
 
import org.apache.lucene.index.Term;
23
 
import org.apache.lucene.search.IndexSearcher;
24
 
import org.apache.lucene.search.spell.Dictionary;
25
 
import org.apache.lucene.search.spell.SpellChecker;
26
 
import org.apache.lucene.search.spell.HighFrequencyDictionary;
27
 
import org.apache.lucene.store.Directory;
28
 
import org.apache.lucene.store.FSDirectory;
29
 
import org.apache.lucene.store.RAMDirectory;
30
 
import org.apache.solr.request.SolrQueryRequest;
31
 
import org.apache.solr.response.SolrQueryResponse;
32
 
import org.apache.solr.common.SolrException;
33
 
import org.apache.solr.common.params.SolrParams;
34
 
import org.apache.solr.common.util.NamedList;
35
 
import org.apache.solr.common.util.SimpleOrderedMap;
36
 
import org.apache.solr.core.SolrCore;
37
 
import org.apache.solr.util.plugin.SolrCoreAware;
38
 
 
39
 
import java.io.File;
40
 
import java.io.IOException;
41
 
import java.net.URL;
42
 
import java.util.Arrays;
43
 
import org.slf4j.Logger;
44
 
import org.slf4j.LoggerFactory;
45
 
 
46
 
/**
47
 
 * Takes a string (e.g. a query string) as the value of the "q" parameter
48
 
 * and looks up alternative spelling suggestions in the spellchecker.
49
 
 * The spellchecker used by this handler is the Lucene contrib SpellChecker.
50
 
 * 
51
 
<style>
52
 
pre.code
53
 
{
54
 
  border: 1pt solid #AEBDCC;
55
 
  background-color: #F3F5F7;
56
 
  padding: 5pt;
57
 
  font-family: courier, monospace;
58
 
  white-space: pre;
59
 
  // begin css 3 or browser specific rules - do not remove!
60
 
  //see: http://forums.techguy.org/archive/index.php/t-249849.html 
61
 
    white-space: pre-wrap;
62
 
    word-wrap: break-word;
63
 
    white-space: -moz-pre-wrap;
64
 
    white-space: -pre-wrap;
65
 
    white-space: -o-pre-wrap;
66
 
   // end css 3 or browser specific rules
67
 
}
68
 
 
69
 
</style>
70
 
 * 
71
 
 * <p>The results identifies the original words echoing it as an entry with the 
72
 
 * name of "words" and original word value.  It 
73
 
 * also identifies if the requested "words" is contained in the index through 
74
 
 * the use of the exist true/false name value. Examples of these output 
75
 
 * parameters in the standard output format is as follows:</p>
76
 
 * <pre class="code">
77
 
&lt;str name="words"&gt;facial&lt;/str&gt;
78
 
&lt;str name="exist"&gt;true&lt;/str&gt; </pre>
79
 
 * 
80
 
 * <p>If a query string parameter of "extendedResults" is used, then each word within the
81
 
 * "q" parameter (seperated by a space or +) will 
82
 
 * be iterated through the spell checker and will be wrapped in an 
83
 
 * NamedList.  Each word will then get its own set of results: words, exists, and
84
 
 * suggestions.</p>
85
 
 * <P><bold>NOTE</bold>: Query terms are simply split on whitespace when using extendedResults mode.  This is may not be adequate.
86
 
 *  See the {@link org.apache.solr.handler.component.SpellCheckComponent} for alternatives.
87
 
 * </P>
88
 
 * <p>Also note that multiword queries will be treated as a single term if extendedResults is false.  This may or may not make sense
89
 
 * depending on how the spelling field was indexed.</p>
90
 
 * 
91
 
 * <p>Examples of the use of the standard ouput (XML) without and with the 
92
 
 * use of the "extendedResults" parameter are as follows.</p>
93
 
 * 
94
 
 * <p> The following URL
95
 
 * examples were configured with the solr.SpellCheckerRequestHandler 
96
 
 * named as "/spellchecker".</p>
97
 
 * 
98
 
 * <p>Without the use of "extendedResults" and one word 
99
 
 * spelled correctly: facial </p>
100
 
 * <pre class="code">http://.../spellchecker?indent=on&onlyMorePopular=true&accuracy=.6&suggestionCount=20&q=facial</pre>
101
 
 * <pre class="code">
102
 
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
103
 
&lt;response&gt;
104
 
 
105
 
&lt;lst name="responseHeader"&gt;
106
 
   &lt;int name="status"&gt;0&lt;/int&gt;
107
 
   &lt;int name="QTime"&gt;6&lt;/int&gt;
108
 
&lt;/lst&gt;
109
 
&lt;str name="words"&gt;facial&lt;/str&gt;
110
 
&lt;str name="exist"&gt;true&lt;/str&gt;
111
 
&lt;arr name="suggestions"&gt;
112
 
   &lt;str&gt;faciale&lt;/str&gt;
113
 
   &lt;str&gt;faucial&lt;/str&gt;
114
 
   &lt;str&gt;fascial&lt;/str&gt;
115
 
   &lt;str&gt;facing&lt;/str&gt;
116
 
   &lt;str&gt;faciei&lt;/str&gt;
117
 
   &lt;str&gt;facialis&lt;/str&gt;
118
 
   &lt;str&gt;social&lt;/str&gt;
119
 
   &lt;str&gt;facile&lt;/str&gt;
120
 
   &lt;str&gt;spacial&lt;/str&gt;
121
 
   &lt;str&gt;glacial&lt;/str&gt;
122
 
   &lt;str&gt;marcial&lt;/str&gt;
123
 
   &lt;str&gt;facies&lt;/str&gt;
124
 
   &lt;str&gt;facio&lt;/str&gt;
125
 
&lt;/arr&gt;
126
 
&lt;/response&gt;   </pre>
127
 
 * 
128
 
 * <p>Without the use of "extendedResults" and two words,  
129
 
 * one spelled correctly and one misspelled: facial salophosphoprotein </p>
130
 
 * <pre class="code">http://.../spellchecker?indent=on&onlyMorePopular=true&accuracy=.6&suggestionCount=20&q=facial+salophosphoprotein</pre>
131
 
 * <pre class="code">
132
 
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
133
 
&lt;response&gt;
134
 
 
135
 
&lt;lst name="responseHeader"&gt;
136
 
   &lt;int name="status"&gt;0&lt;/int&gt;
137
 
   &lt;int name="QTime"&gt;18&lt;/int&gt;
138
 
&lt;/lst&gt;
139
 
&lt;str name="words"&gt;facial salophosphoprotein&lt;/str&gt;
140
 
&lt;str name="exist"&gt;false&lt;/str&gt;
141
 
&lt;arr name="suggestions"&gt;
142
 
   &lt;str&gt;sialophosphoprotein&lt;/str&gt;
143
 
&lt;/arr&gt;
144
 
&lt;/response&gt;  </pre>
145
 
 * 
146
 
 * 
147
 
 * <p>With the use of "extendedResults" and two words,  
148
 
 * one spelled correctly and one misspelled: facial salophosphoprotein </p>
149
 
 * <pre class="code">http://.../spellchecker?indent=on&onlyMorePopular=true&accuracy=.6&suggestionCount=20&extendedResults=true&q=facial+salophosphoprotein</pre>
150
 
 * <pre class="code">
151
 
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
152
 
&lt;response&gt;
153
 
 
154
 
&lt;lst name="responseHeader"&gt;
155
 
   &lt;int name="status"&gt;0&lt;/int&gt;
156
 
   &lt;int name="QTime"&gt;23&lt;/int&gt;
157
 
&lt;/lst&gt;
158
 
&lt;lst name="result"&gt;
159
 
  &lt;lst name="facial"&gt;
160
 
    &lt;int name="frequency"&gt;1&lt;/int&gt;
161
 
    &lt;lst name="suggestions"&gt;
162
 
      &lt;lst name="faciale"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
163
 
      &lt;lst name="faucial"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
164
 
      &lt;lst name="fascial"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
165
 
      &lt;lst name="facing"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
166
 
      &lt;lst name="faciei"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
167
 
      &lt;lst name="facialis"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
168
 
      &lt;lst name="social"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
169
 
      &lt;lst name="facile"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
170
 
      &lt;lst name="spacial"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
171
 
      &lt;lst name="glacial"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
172
 
      &lt;lst name="marcial"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
173
 
      &lt;lst name="facies"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
174
 
      &lt;lst name="facio"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
175
 
    &lt;/lst&gt;
176
 
  &lt;/lst&gt;
177
 
  &lt;lst name="salophosphoprotein"&gt;
178
 
    &lt;int name="frequency"&gt;0&lt;/int&gt;
179
 
    &lt;lst name="suggestions"&gt; 
180
 
      &lt;lst name="sialophosphoprotein"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
181
 
      &lt;lst name="phosphoprotein"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
182
 
      &lt;lst name="phosphoproteins"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
183
 
      &lt;lst name="alphalipoprotein"&gt;&lt;int name="frequency"&gt;1&lt;/int&gt;&lt;/lst&gt;
184
 
    &lt;/lst&gt;
185
 
  &lt;/lst&gt;
186
 
&lt;/lst&gt;
187
 
&lt;/response&gt;  </pre>
188
 
 
189
 
 * 
190
 
 * @see <a href="http://wiki.apache.org/jakarta-lucene/SpellChecker">The Lucene Spellchecker documentation</a>
191
 
 *
192
 
 *
193
 
 * @deprecated Use {@link org.apache.solr.handler.component.SpellCheckComponent} instead.
194
 
 *
195
 
 * See also https://issues.apache.org/jira/browse/SOLR-474 and https://issues.apache.org/jira/browse/SOLR-485
196
 
 *
197
 
 */
198
 
@Deprecated
199
 
public class SpellCheckerRequestHandler extends RequestHandlerBase implements SolrCoreAware {
200
 
 
201
 
  private static Logger log = LoggerFactory.getLogger(SpellCheckerRequestHandler.class);
202
 
  
203
 
  private SpellChecker spellChecker;
204
 
  
205
 
  /*
206
 
   * From http://wiki.apache.org/jakarta-lucene/SpellChecker
207
 
   * If reader and restrictToField are both not null:
208
 
   * 1. The returned words are restricted only to the words presents in the field
209
 
   * "restrictToField "of the Lucene Index "reader".
210
 
   *
211
 
   * 2. The list is also sorted with a second criterium: the popularity (the
212
 
   * frequence) of the word in the user field.
213
 
   *
214
 
   * 3. If "onlyMorePopular" is true and the mispelled word exist in the user field,
215
 
   * return only the words more frequent than this.
216
 
   * 
217
 
   */
218
 
 
219
 
  protected Directory spellcheckerIndexDir = new RAMDirectory();
220
 
  protected String dirDescription = "(ramdir)";
221
 
  protected String termSourceField;
222
 
 
223
 
  protected static final String PREFIX = "sp.";
224
 
  protected static final String QUERY_PREFIX = PREFIX + "query.";
225
 
  protected static final String DICTIONARY_PREFIX = PREFIX + "dictionary.";
226
 
 
227
 
  protected static final String SOURCE_FIELD = DICTIONARY_PREFIX + "termSourceField";
228
 
  protected static final String INDEX_DIR = DICTIONARY_PREFIX + "indexDir";
229
 
  protected static final String THRESHOLD = DICTIONARY_PREFIX + "threshold";
230
 
 
231
 
  protected static final String ACCURACY = QUERY_PREFIX + "accuracy";
232
 
  protected static final String SUGGESTIONS = QUERY_PREFIX + "suggestionCount";
233
 
  protected static final String POPULAR = QUERY_PREFIX + "onlyMorePopular";
234
 
  protected static final String EXTENDED = QUERY_PREFIX + "extendedResults";
235
 
 
236
 
  protected static final float DEFAULT_ACCURACY = 0.5f;
237
 
  protected static final int DEFAULT_SUGGESTION_COUNT = 1;
238
 
  protected static final boolean DEFAULT_MORE_POPULAR = false;
239
 
  protected static final boolean DEFAULT_EXTENDED_RESULTS = false;
240
 
  protected static final float DEFAULT_DICTIONARY_THRESHOLD = 0.0f;
241
 
 
242
 
  protected SolrParams args = null;
243
 
  
244
 
  @Override
245
 
  public void init(NamedList args) {
246
 
    super.init(args);
247
 
    this.args = SolrParams.toSolrParams(args);
248
 
  }
249
 
 
250
 
  public void inform(SolrCore core) 
251
 
  {
252
 
    termSourceField = args.get(SOURCE_FIELD, args.get("termSourceField"));
253
 
    try {
254
 
      String dir = args.get(INDEX_DIR, args.get("spellcheckerIndexDir"));
255
 
      if (null != dir) {
256
 
        File f = new File(dir);
257
 
        if ( ! f.isAbsolute() ) {
258
 
          f = new File(core.getDataDir(), dir);
259
 
        }
260
 
        dirDescription = f.getAbsolutePath();
261
 
        log.info("using spell directory: " + dirDescription);
262
 
        spellcheckerIndexDir = FSDirectory.open(f);
263
 
      } else {
264
 
        log.info("using RAM based spell directory");
265
 
      }
266
 
      spellChecker = new SpellChecker(spellcheckerIndexDir);
267
 
    } catch (IOException e) {
268
 
      throw new RuntimeException("Cannot open SpellChecker index", e);
269
 
    }
270
 
  }
271
 
 
272
 
  /**
273
 
   * Processes the following query string parameters: q, extendedResults, cmd rebuild,
274
 
   * cmd reopen, accuracy, suggestionCount, restrictToField, and onlyMorePopular.
275
 
   */
276
 
  @Override
277
 
  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)
278
 
    throws Exception {
279
 
    SolrParams p = req.getParams();
280
 
    String words = p.get("q");
281
 
    String cmd = p.get("cmd");
282
 
    if (cmd != null) {
283
 
      cmd = cmd.trim();
284
 
      if (cmd.equals("rebuild")) {
285
 
        rebuild(req);
286
 
        rsp.add("cmdExecuted","rebuild");
287
 
      } else if (cmd.equals("reopen")) {
288
 
        reopen();
289
 
        rsp.add("cmdExecuted","reopen");
290
 
      } else {
291
 
        throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Unrecognized Command: " + cmd);
292
 
      }
293
 
    }
294
 
 
295
 
    // empty query string
296
 
    if (null == words || "".equals(words.trim())) {
297
 
      return;
298
 
    }
299
 
 
300
 
    IndexReader indexReader = null;
301
 
    String suggestionField = null;
302
 
    Float accuracy;
303
 
    int numSug;
304
 
    boolean onlyMorePopular;
305
 
    boolean extendedResults;
306
 
    try {
307
 
      accuracy = p.getFloat(ACCURACY, p.getFloat("accuracy", DEFAULT_ACCURACY));
308
 
      spellChecker.setAccuracy(accuracy);
309
 
    } catch (NumberFormatException e) {
310
 
      throw new RuntimeException("Accuracy must be a valid positive float", e);
311
 
    }
312
 
    try {
313
 
      numSug = p.getInt(SUGGESTIONS, p.getInt("suggestionCount", DEFAULT_SUGGESTION_COUNT));
314
 
    } catch (NumberFormatException e) {
315
 
      throw new RuntimeException("Spelling suggestion count must be a valid positive integer", e);
316
 
    }
317
 
    try {
318
 
      onlyMorePopular = p.getBool(POPULAR, DEFAULT_MORE_POPULAR);
319
 
    } catch (SolrException e) {
320
 
      throw new RuntimeException("'Only more popular' must be a valid boolean", e);
321
 
    }
322
 
    try {
323
 
      extendedResults = p.getBool(EXTENDED, DEFAULT_EXTENDED_RESULTS);
324
 
    } catch (SolrException e) {
325
 
      throw new RuntimeException("'Extended results' must be a valid boolean", e);
326
 
    }
327
 
 
328
 
    // when searching for more popular, a non null index-reader and
329
 
    // restricted-field are required
330
 
    if (onlyMorePopular || extendedResults) {
331
 
      indexReader = req.getSearcher().getReader();
332
 
      suggestionField = termSourceField;
333
 
    }
334
 
 
335
 
    if (extendedResults) {
336
 
 
337
 
      rsp.add("numDocs", indexReader.numDocs());
338
 
 
339
 
      SimpleOrderedMap<Object> results = new SimpleOrderedMap<Object>();
340
 
      String[] wordz = words.split(" ");
341
 
      for (String word : wordz)
342
 
      {
343
 
        SimpleOrderedMap<Object> nl = new SimpleOrderedMap<Object>();
344
 
        nl.add("frequency", indexReader.docFreq(new Term(suggestionField, word)));
345
 
        String[] suggestions =
346
 
          spellChecker.suggestSimilar(word, numSug,
347
 
          indexReader, suggestionField, onlyMorePopular);
348
 
 
349
 
        // suggestion array
350
 
        NamedList<Object> sa = new NamedList<Object>();
351
 
        for (int i=0; i<suggestions.length; i++) {
352
 
          // suggestion item
353
 
          SimpleOrderedMap<Object> si = new SimpleOrderedMap<Object>();
354
 
          si.add("frequency", indexReader.docFreq(new Term(termSourceField, suggestions[i])));
355
 
          sa.add(suggestions[i], si);
356
 
        }
357
 
        nl.add("suggestions", sa);
358
 
        results.add(word, nl);
359
 
      }
360
 
      rsp.add( "result", results );
361
 
 
362
 
    } else {
363
 
      rsp.add("words", words);
364
 
      if (spellChecker.exist(words)) {
365
 
        rsp.add("exist","true");
366
 
      } else {
367
 
        rsp.add("exist","false");
368
 
      }
369
 
      String[] suggestions =
370
 
        spellChecker.suggestSimilar(words, numSug,
371
 
                                    indexReader, suggestionField,
372
 
                                    onlyMorePopular);
373
 
 
374
 
      rsp.add("suggestions", Arrays.asList(suggestions));
375
 
    }
376
 
  }
377
 
 
378
 
  /** Returns a dictionary to be used when building the spell-checker index.
379
 
   * Override the method for custom dictionary
380
 
   */
381
 
  protected Dictionary getDictionary(SolrQueryRequest req) {
382
 
    float threshold;
383
 
    try {
384
 
      threshold = req.getParams().getFloat(THRESHOLD, DEFAULT_DICTIONARY_THRESHOLD);
385
 
    } catch (NumberFormatException e) {
386
 
      throw new RuntimeException("Threshold must be a valid positive float", e);
387
 
    }
388
 
    IndexReader indexReader = req.getSearcher().getReader();
389
 
    return new HighFrequencyDictionary(indexReader, termSourceField, threshold);
390
 
  }
391
 
 
392
 
  /** Rebuilds the SpellChecker index using values from the <code>termSourceField</code> from the
393
 
   * index pointed to by the current {@link IndexSearcher}.
394
 
   * Any word appearing in less that thresh documents will not be added to the spellcheck index.
395
 
   */
396
 
  private void rebuild(SolrQueryRequest req) throws IOException, SolrException {
397
 
    if (null == termSourceField) {
398
 
      throw new SolrException
399
 
        (SolrException.ErrorCode.SERVER_ERROR, "can't rebuild spellchecker index without termSourceField configured");
400
 
    }
401
 
 
402
 
    Dictionary dictionary = getDictionary(req);
403
 
    spellChecker.clearIndex();
404
 
    spellChecker.indexDictionary(dictionary, new IndexWriterConfig(req.getCore().getSolrConfig().luceneMatchVersion, null), false);
405
 
    reopen();
406
 
  }
407
 
  
408
 
  /**
409
 
   * Reopens the SpellChecker index directory.
410
 
   * Useful if an external process is responsible for building
411
 
   * the spell checker index.
412
 
   */
413
 
  private void reopen() throws IOException {
414
 
    spellChecker.setSpellIndex(spellcheckerIndexDir);
415
 
  }
416
 
 
417
 
  //////////////////////// SolrInfoMBeans methods //////////////////////
418
 
 
419
 
  @Override
420
 
  public String getVersion() {
421
 
    return "$Revision: 1197478 $";
422
 
  }
423
 
 
424
 
  @Override
425
 
  public String getDescription() {
426
 
    return "The SpellChecker Solr request handler for SpellChecker index: " + dirDescription;
427
 
  }
428
 
 
429
 
  @Override
430
 
  public String getSourceId() {
431
 
    return "$Id: SpellCheckerRequestHandler.java 1197478 2011-11-04 10:10:03Z rmuir $";
432
 
  }
433
 
 
434
 
  @Override
435
 
  public String getSource() {
436
 
    return "$URL: http://svn.apache.org/repos/asf/lucene/dev/tags/lucene_solr_3_5_0/solr/core/src/java/org/apache/solr/handler/SpellCheckerRequestHandler.java $";
437
 
  }
438
 
 
439
 
  @Override
440
 
  public URL[] getDocs() {
441
 
    return null;
442
 
  }
443
 
}