File D:\maven-qcr-plugin-testsunitaires\qcr4java\h2-1.3.170-sources\target\site\qc\lecode\fulltext\FullTextLucene.java.html

1 /*
2 * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
3 * Version 1.0, and under the Eclipse Public License, Version 1.0
4 * (http://h2database.com/html/license.html).
5 * Initial Developer: H2 Group
6 */
7 package org.h2.fulltext;
8
9 import java.io.IOException;
10 import java.sql.Connection;
11 import java.sql.DatabaseMetaData;
12 import java.sql.PreparedStatement;
13 import java.sql.ResultSet;
14 import java.sql.SQLException;
15 import java.sql.Statement;
16 import java.util.ArrayList;
17 import java.util.HashMap;
18 import org.apache.lucene.analysis.Analyzer;
19 import org.apache.lucene.analysis.standard.StandardAnalyzer;
20 import org.apache.lucene.document.DateTools;
21 import org.apache.lucene.document.Document;
22 import org.apache.lucene.document.Field;
23 import org.apache.lucene.index.IndexReader;
24 import org.apache.lucene.index.Term;
25 import org.apache.lucene.queryParser.QueryParser;
26 import org.apache.lucene.search.IndexSearcher;
27 import org.apache.lucene.search.Query;
28 import org.apache.lucene.search.Searcher;
29 import org.h2.api.Trigger;
30 import org.h2.command.Parser;
31 import org.h2.engine.Session;
32 import org.h2.expression.ExpressionColumn;
33 import org.h2.jdbc.JdbcConnection;
34 import org.h2.store.fs.FileUtils;
35 import org.h2.tools.SimpleResultSet;
36 import org.h2.util.JdbcUtils;
37 import org.h2.util.New;
38 import org.h2.util.StatementBuilder;
39 import org.h2.util.StringUtils;
40 import org.h2.util.Utils;
41 /*## LUCENE2 ##
42 import org.apache.lucene.index.IndexModifier;
43 import org.apache.lucene.search.Hits;
44 //*/
45 //## LUCENE3 ##
46 import java.io.File;
47 import org.apache.lucene.search.ScoreDoc;
48 import org.apache.lucene.search.TopDocs;
49 import org.apache.lucene.store.FSDirectory;
50 import org.apache.lucene.store.Directory;
51 import org.apache.lucene.store.RAMDirectory;
52 import org.apache.lucene.util.Version;
53 import org.apache.lucene.index.IndexWriter;
54 //*/
55
56 /**
57 * This class implements the full text search based on Apache Lucene.
58 * Most methods can be called using SQL statements as well.
59 */
60 public class FullTextLucene extends FullText {
61
62 /**
63 * Whether the text content should be stored in the Lucene index.
64 */
65 protected static final boolean STORE_DOCUMENT_TEXT_IN_INDEX = Utils.getProperty("h2.storeDocumentTextInIndex", false);
66
67 private static final HashMap<String, IndexAccess> INDEX_ACCESS = New.hashMap();
68 private static final String TRIGGER_PREFIX = "FTL_";
69 private static final String SCHEMA = "FTL";
70 private static final String LUCENE_FIELD_DATA = "_DATA";
71 private static final String LUCENE_FIELD_QUERY = "_QUERY";
72 private static final String LUCENE_FIELD_MODIFIED = "_modified";
73 private static final String LUCENE_FIELD_COLUMN_PREFIX = "_";
74
75 /**
76 * The prefix for a in-memory path. This prefix is only used internally
77 * within this class and not related to the database URL.
78 */
79 private static final String IN_MEMORY_PREFIX = "mem:";
80
81 /**
82 * Initializes full text search functionality for this database. This adds
83 * the following Java functions to the database:
84 * <ul>
85 * <li>FTL_CREATE_INDEX(schemaNameString, tableNameString,
86 * columnListString)</li>
87 * <li>FTL_SEARCH(queryString, limitInt, offsetInt): result set</li>
88 * <li>FTL_REINDEX()</li>
89 * <li>FTL_DROP_ALL()</li>
90 * </ul>
91 * It also adds a schema FTL to the database where bookkeeping information
92 * is stored. This function may be called from a Java application, or by
93 * using the SQL statements:
94 *
95 * <pre>
96 * CREATE ALIAS IF NOT EXISTS FTL_INIT FOR
97 * &quot;org.h2.fulltext.FullTextLucene.init&quot;;
98 * CALL FTL_INIT();
99 * </pre>
100 *
101 * @param conn the connection
102 */
103 public static void init(Connection conn) throws SQLException {
104 Statement stat = conn.createStatement();
105 stat.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA);
106 stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA
107 + ".INDEXES(SCHEMA VARCHAR, TABLE VARCHAR, COLUMNS VARCHAR, PRIMARY KEY(SCHEMA, TABLE))");
108 stat.execute("CREATE ALIAS IF NOT EXISTS FTL_CREATE_INDEX FOR \"" + FullTextLucene.class.getName() + ".createIndex\"");
109 stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH FOR \"" + FullTextLucene.class.getName() + ".search\"");
110 stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH_DATA FOR \"" + FullTextLucene.class.getName() + ".searchData\"");
111 stat.execute("CREATE ALIAS IF NOT EXISTS FTL_REINDEX FOR \"" + FullTextLucene.class.getName() + ".reindex\"");
112 stat.execute("CREATE ALIAS IF NOT EXISTS FTL_DROP_ALL FOR \"" + FullTextLucene.class.getName() + ".dropAll\"");
113 try {
114 getIndexAccess(conn);
115 } catch (SQLException e) {
116 throw convertException(e);
117 }
118 }
119
120 /**
121 * Create a new full text index for a table and column list. Each table may
122 * only have one index at any time.
123 *
124 * @param conn the connection
125 * @param schema the schema name of the table (case sensitive)
126 * @param table the table name (case sensitive)
127 * @param columnList the column list (null for all columns)
128 */
129 public static void createIndex(Connection conn, String schema, String table, String columnList) throws SQLException {
130 init(conn);
131 PreparedStatement prep = conn.prepareStatement("INSERT INTO " + SCHEMA
132 + ".INDEXES(SCHEMA, TABLE, COLUMNS) VALUES(?, ?, ?)");
133 prep.setString(1, schema);
134 prep.setString(2, table);
135 prep.setString(3, columnList);
136 prep.execute();
137 createTrigger(conn, schema, table);
138 indexExistingRows(conn, schema, table);
139 }
140
141 /**
142 * Re-creates the full text index for this database. Calling this method is
143 * usually not needed, as the index is kept up-to-date automatically.
144 *
145 * @param conn the connection
146 */
147 public static void reindex(Connection conn) throws SQLException {
148 init(conn);
149 removeAllTriggers(conn, TRIGGER_PREFIX);
150 removeIndexFiles(conn);
151 Statement stat = conn.createStatement();
152 ResultSet rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".INDEXES");
153 while (rs.next()) {
154 String schema = rs.getString("SCHEMA");
155 String table = rs.getString("TABLE");
156 createTrigger(conn, schema, table);
157 indexExistingRows(conn, schema, table);
158 }
159 }
160
161 /**
162 * Drops all full text indexes from the database.
163 *
164 * @param conn the connection
165 */
166 public static void dropAll(Connection conn) throws SQLException {
167 Statement stat = conn.createStatement();
168 stat.execute("DROP SCHEMA IF EXISTS " + SCHEMA);
169 removeAllTriggers(conn, TRIGGER_PREFIX);
170 removeIndexFiles(conn);
171 }
172
173 /**
174 * Searches from the full text index for this database.
175 * The returned result set has the following column:
176 * <ul><li>QUERY (varchar): the query to use to get the data.
177 * The query does not include 'SELECT * FROM '. Example:
178 * PUBLIC.TEST WHERE ID = 1
179 * </li><li>SCORE (float) the relevance score as returned by Lucene.
180 * </li></ul>
181 *
182 * @param conn the connection
183 * @param text the search query
184 * @param limit the maximum number of rows or 0 for no limit
185 * @param offset the offset or 0 for no offset
186 * @return the result set
187 */
188 public static ResultSet search(Connection conn, String text, int limit, int offset) throws SQLException {
189 return search(conn, text, limit, offset, false);
190 }
191
192 /**
193 * Searches from the full text index for this database. The result contains
194 * the primary key data as an array. The returned result set has the
195 * following columns:
196 * <ul>
197 * <li>SCHEMA (varchar): the schema name. Example: PUBLIC </li>
198 * <li>TABLE (varchar): the table name. Example: TEST </li>
199 * <li>COLUMNS (array of varchar): comma separated list of quoted column
200 * names. The column names are quoted if necessary. Example: (ID) </li>
201 * <li>KEYS (array of values): comma separated list of values. Example: (1)
202 * </li><li>SCORE (float) the relevance score as returned by Lucene.
203 * </li>
204 * </ul>
205 *
206 * @param conn the connection
207 * @param text the search query
208 * @param limit the maximum number of rows or 0 for no limit
209 * @param offset the offset or 0 for no offset
210 * @return the result set
211 */
212 public static ResultSet searchData(Connection conn, String text, int limit, int offset) throws SQLException {
213 return search(conn, text, limit, offset, true);
214 }
215
216 /**
217 * Convert an exception to a fulltext exception.
218 *
219 * @param e the original exception
220 * @return the converted SQL exception
221 */
222 protected static SQLException convertException(Exception e) {
223 SQLException e2 = new SQLException("Error while indexing document", "FULLTEXT");
224 e2.initCause(e);
225 return e2;
226 }
227
228 /**
229 * Create the trigger.
230 *
231 * @param conn the database connection
232 * @param schema the schema name
233 * @param table the table name
234 */
235 protected static void createTrigger(Connection conn, String schema, String table) throws SQLException {
236 Statement stat = conn.createStatement();
237 String trigger = StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(TRIGGER_PREFIX + table);
238 stat.execute("DROP TRIGGER IF EXISTS " + trigger);
239 StringBuilder buff = new StringBuilder("CREATE TRIGGER IF NOT EXISTS ");
240 // the trigger is also called on rollback because transaction rollback
241 // will not undo the changes in the Lucene index
242 buff.append(trigger).
243 append(" AFTER INSERT, UPDATE, DELETE, ROLLBACK ON ").
244 append(StringUtils.quoteIdentifier(schema)).
245 append('.').
246 append(StringUtils.quoteIdentifier(table)).
247 append(" FOR EACH ROW CALL \"").
248 append(FullTextLucene.FullTextTrigger.class.getName()).
249 append('\"');
250 stat.execute(buff.toString());
251 }
252
253 /**
254 * Get the index writer/searcher wrapper for the given connection.
255 *
256 * @param conn the connection
257 * @return the index access wrapper
258 */
259 protected static IndexAccess getIndexAccess(Connection conn) throws SQLException {
260 String path = getIndexPath(conn);
261 synchronized (INDEX_ACCESS) {
262 IndexAccess access = INDEX_ACCESS.get(path);
263 if (access == null) {
264 try {
265 /*## LUCENE2 ##
266 boolean recreate = !IndexReader.indexExists(path);
267 Analyzer analyzer = new StandardAnalyzer();
268 access = new IndexAccess();
269 access.modifier = new IndexModifier(path, analyzer, recreate);
270 //*/
271 //## LUCENE3 ##
272 Directory indexDir = path.startsWith(IN_MEMORY_PREFIX) ? new RAMDirectory() : FSDirectory.open(new File(path));
273 boolean recreate = !IndexReader.indexExists(indexDir);
274 Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
275 IndexWriter writer = new IndexWriter(indexDir, analyzer,
276 recreate, IndexWriter.MaxFieldLength.UNLIMITED);
277 //see http://wiki.apache.org/lucene-java/NearRealtimeSearch
278 IndexReader reader = writer.getReader();
279 access = new IndexAccess();
280 access.writer = writer;
281 access.reader = reader;
282 access.searcher = new IndexSearcher(reader);
283 //*/
284 } catch (IOException e) {
285 throw convertException(e);
286 }
287 INDEX_ACCESS.put(path, access);
288 }
289 return access;
290 }
291 }
292
293 /**
294 * Get the path of the Lucene index for this database.
295 *
296 * @param conn the database connection
297 * @return the path
298 */
299 protected static String getIndexPath(Connection conn) throws SQLException {
300 Statement stat = conn.createStatement();
301 ResultSet rs = stat.executeQuery("CALL DATABASE_PATH()");
302 rs.next();
303 String path = rs.getString(1);
304 if (path == null) {
305 /*## LUCENE2 ##
306 throw throwException("Fulltext search for in-memory databases " +
307 "is not supported with Lucene 2. Please use Lucene 3 instead.");
308 //*/
309 //## LUCENE3 ##
310 return IN_MEMORY_PREFIX + conn.getCatalog();
311 //*/
312 }
313 int index = path.lastIndexOf(':');
314 // position 1 means a windows drive letter is used, ignore that
315 if (index > 1) {
316 path = path.substring(index + 1);
317 }
318 rs.close();
319 return path;
320 }
321
322 /**
323 * Add the existing data to the index.
324 *
325 * @param conn the database connection
326 * @param schema the schema name
327 * @param table the table name
328 */
329 protected static void indexExistingRows(Connection conn, String schema, String table) throws SQLException {
330 FullTextLucene.FullTextTrigger existing = new FullTextLucene.FullTextTrigger();
331 existing.init(conn, schema, null, table, false, Trigger.INSERT);
332 String sql = "SELECT * FROM " + StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table);
333 ResultSet rs = conn.createStatement().executeQuery(sql);
334 int columnCount = rs.getMetaData().getColumnCount();
335 while (rs.next()) {
336 Object[] row = new Object[columnCount];
337 for (int i = 0; i < columnCount; i++) {
338 row[i] = rs.getObject(i + 1);
339 }
340 existing.insert(row, false);
341 }
342 existing.commitIndex();
343 }
344
345 private static void removeIndexFiles(Connection conn) throws SQLException {
346 String path = getIndexPath(conn);
347 IndexAccess access = INDEX_ACCESS.get(path);
348 if (access != null) {
349 removeIndexAccess(access, path);
350 }
351 if (!path.startsWith(IN_MEMORY_PREFIX)) {
352 FileUtils.deleteRecursive(path, false);
353 }
354 }
355
356 /**
357 * Close the index writer and searcher and remove them from the index access set.
358 *
359 * @param access the index writer/searcher wrapper
360 * @param indexPath the index path
361 */
362 protected static void removeIndexAccess(IndexAccess access, String indexPath) throws SQLException {
363 synchronized (INDEX_ACCESS) {
364 try {
365 INDEX_ACCESS.remove(indexPath);
366 /*## LUCENE2 ##
367 access.modifier.flush();
368 access.modifier.close();
369 //*/
370 //## LUCENE3 ##
371 access.searcher.close();
372 access.reader.close();
373 access.writer.close();
374 //*/
375 } catch (Exception e) {
376 throw convertException(e);
377 }
378 }
379 }
380
381 /**
382 * Do the search.
383 *
384 * @param conn the database connection
385 * @param text the query
386 * @param limit the limit
387 * @param offset the offset
388 * @param data whether the raw data should be returned
389 * @return the result set
390 */
391 protected static ResultSet search(Connection conn, String text,
392 int limit, int offset, boolean data) throws SQLException {
393 SimpleResultSet result = createResultSet(data);
394 if (conn.getMetaData().getURL().startsWith("jdbc:columnlist:")) {
395 // this is just to query the result set columns
396 return result;
397 }
398 if (text == null || text.trim().length() == 0) {
399 return result;
400 }
401 try {
402 IndexAccess access = getIndexAccess(conn);
403 /*## LUCENE2 ##
404 access.modifier.flush();
405 String path = getIndexPath(conn);
406 IndexReader reader = IndexReader.open(path);
407 Analyzer analyzer = new StandardAnalyzer();
408 Searcher searcher = new IndexSearcher(reader);
409 QueryParser parser = new QueryParser(LUCENE_FIELD_DATA, analyzer);
410 Query query = parser.parse(text);
411 Hits hits = searcher.search(query);
412 int max = hits.length();
413 if (limit == 0) {
414 limit = max;
415 }
416 for (int i = 0; i < limit && i + offset < max; i++) {
417 Document doc = hits.doc(i + offset);
418 float score = hits.score(i + offset);
419 //*/
420 //## LUCENE3 ##
421 // take a reference as the searcher may change
422 Searcher searcher = access.searcher;
423 // reuse the same analyzer; it's thread-safe;
424 // also allows subclasses to control the analyzer used.
425 Analyzer analyzer = access.writer.getAnalyzer();
426 QueryParser parser = new QueryParser(Version.LUCENE_30,
427 LUCENE_FIELD_DATA, analyzer);
428 Query query = parser.parse(text);
429 // Lucene 3 insists on a hard limit and will not provide
430 // a total hits value. Take at least 100 which is
431 // an optimal limit for Lucene as any more
432 // will trigger writing results to disk.
433 int maxResults = (limit == 0 ? 100 : limit) + offset;
434 TopDocs docs = searcher.search(query, maxResults);
435 if (limit == 0) {
436 limit = docs.totalHits;
437 }
438 for (int i = 0, len = docs.scoreDocs.length;
439 i < limit && i + offset < docs.totalHits
440 && i + offset < len; i++) {
441 ScoreDoc sd = docs.scoreDocs[i + offset];
442 Document doc = searcher.doc(sd.doc);
443 float score = sd.score;
444 //*/
445 String q = doc.get(LUCENE_FIELD_QUERY);
446 if (data) {
447 int idx = q.indexOf(" WHERE ");
448 JdbcConnection c = (JdbcConnection) conn;
449 Session session = (Session) c.getSession();
450 Parser p = new Parser(session);
451 String tab = q.substring(0, idx);
452 ExpressionColumn expr = (ExpressionColumn) p.parseExpression(tab);
453 String schemaName = expr.getOriginalTableAliasName();
454 String tableName = expr.getColumnName();
455 q = q.substring(idx + " WHERE ".length());
456 Object[][] columnData = parseKey(conn, q);
457 result.addRow(
458 schemaName,
459 tableName,
460 columnData[0],
461 columnData[1],
462 score);
463 } else {
464 result.addRow(q, score);
465 }
466 }
467 /*## LUCENE2 ##
468 // TODO keep it open if possible
469 reader.close();
470 //*/
471 } catch (Exception e) {
472 throw convertException(e);
473 }
474 return result;
475 }
476
477 /**
478 * Trigger updates the index when a inserting, updating, or deleting a row.
479 */
480 public static class FullTextTrigger implements Trigger {
481
482 protected String schema;
483 protected String table;
484 protected int[] keys;
485 protected int[] indexColumns;
486 protected String[] columns;
487 protected int[] columnTypes;
488 protected String indexPath;
489 protected IndexAccess indexAccess;
490
491 /**
492 * INTERNAL
493 */
494 public void init(Connection conn, String schemaName, String triggerName,
495 String tableName, boolean before, int type) throws SQLException {
496 this.schema = schemaName;
497 this.table = tableName;
498 this.indexPath = getIndexPath(conn);
499 this.indexAccess = getIndexAccess(conn);
500 ArrayList<String> keyList = New.arrayList();
501 DatabaseMetaData meta = conn.getMetaData();
502 ResultSet rs = meta.getColumns(null,
503 JdbcUtils.escapeMetaDataPattern(schemaName),
504 JdbcUtils.escapeMetaDataPattern(tableName),
505 null);
506 ArrayList<String> columnList = New.arrayList();
507 while (rs.next()) {
508 columnList.add(rs.getString("COLUMN_NAME"));
509 }
510 columnTypes = new int[columnList.size()];
511 columns = new String[columnList.size()];
512 columnList.toArray(columns);
513 rs = meta.getColumns(null,
514 JdbcUtils.escapeMetaDataPattern(schemaName),
515 JdbcUtils.escapeMetaDataPattern(tableName),
516 null);
517 for (int i = 0; rs.next(); i++) {
518 columnTypes[i] = rs.getInt("DATA_TYPE");
519 }
520 if (keyList.size() == 0) {
521 rs = meta.getPrimaryKeys(null,
522 JdbcUtils.escapeMetaDataPattern(schemaName),
523 tableName);
524 while (rs.next()) {
525 keyList.add(rs.getString("COLUMN_NAME"));
526 }
527 }
528 if (keyList.size() == 0) {
529 throw throwException("No primary key for table " + tableName);
530 }
531 ArrayList<String> indexList = New.arrayList();
532 PreparedStatement prep = conn.prepareStatement(
533 "SELECT COLUMNS FROM " + SCHEMA
534 + ".INDEXES WHERE SCHEMA=? AND TABLE=?");
535 prep.setString(1, schemaName);
536 prep.setString(2, tableName);
537 rs = prep.executeQuery();
538 if (rs.next()) {
539 String cols = rs.getString(1);
540 if (cols != null) {
541 for (String s : StringUtils.arraySplit(cols, ',', true)) {
542 indexList.add(s);
543 }
544 }
545 }
546 if (indexList.size() == 0) {
547 indexList.addAll(columnList);
548 }
549 keys = new int[keyList.size()];
550 setColumns(keys, keyList, columnList);
551 indexColumns = new int[indexList.size()];
552 setColumns(indexColumns, indexList, columnList);
553 }
554
555 /**
556 * INTERNAL
557 */
558 public void fire(Connection conn, Object[] oldRow, Object[] newRow)
559 throws SQLException {
560 if (oldRow != null) {
561 if (newRow != null) {
562 // update
563 if (hasChanged(oldRow, newRow, indexColumns)) {
564 delete(oldRow, false);
565 insert(newRow, true);
566 }
567 } else {
568 // delete
569 delete(oldRow, true);
570 }
571 } else if (newRow != null) {
572 // insert
573 insert(newRow, true);
574 }
575 }
576
577 /**
578 * INTERNAL
579 */
580 public void close() throws SQLException {
581 if (indexAccess != null) {
582 removeIndexAccess(indexAccess, indexPath);
583 indexAccess = null;
584 }
585 }
586
587 /**
588 * INTERNAL
589 */
590 public void remove() {
591 // ignore
592 }
593
594 /**
595 * Commit all changes to the Lucene index.
596 */
597 void commitIndex() throws SQLException {
598 try {
599 indexAccess.writer.commit();
600 // recreate Searcher with the IndexWriter's reader.
601 indexAccess.searcher.close();
602 indexAccess.reader.close();
603 IndexReader reader = indexAccess.writer.getReader();
604 indexAccess.reader = reader;
605 indexAccess.searcher = new IndexSearcher(reader);
606 } catch (IOException e) {
607 throw convertException(e);
608 }
609 }
610
611 /**
612 * Add a row to the index.
613 *
614 * @param row the row
615 * @param commitIndex whether to commit the changes to the Lucene index
616 */
617 protected void insert(Object[] row, boolean commitIndex) throws SQLException {
618 /*## LUCENE2 ##
619 String query = getQuery(row);
620 Document doc = new Document();
621 doc.add(new Field(LUCENE_FIELD_QUERY, query,
622 Field.Store.YES, Field.Index.UN_TOKENIZED));
623 long time = System.currentTimeMillis();
624 doc.add(new Field(LUCENE_FIELD_MODIFIED,
625 DateTools.timeToString(time, DateTools.Resolution.SECOND),
626 Field.Store.YES, Field.Index.UN_TOKENIZED));
627 StatementBuilder buff = new StatementBuilder();
628 for (int index : indexColumns) {
629 String columnName = columns[index];
630 String data = asString(row[index], columnTypes[index]);
631 // column names that start with _ must be escaped to avoid conflicts
632 // with internal field names (_DATA, _QUERY, _modified)
633 if (columnName.startsWith(LUCENE_FIELD_COLUMN_PREFIX)) {
634 columnName = LUCENE_FIELD_COLUMN_PREFIX + columnName;
635 }
636 doc.add(new Field(columnName, data,
637 Field.Store.NO, Field.Index.TOKENIZED));
638 buff.appendExceptFirst(" ");
639 buff.append(data);
640 }
641 Field.Store storeText = STORE_DOCUMENT_TEXT_IN_INDEX ?
642 Field.Store.YES : Field.Store.NO;
643 doc.add(new Field(LUCENE_FIELD_DATA, buff.toString(), storeText,
644 Field.Index.TOKENIZED));
645 try {
646 indexAccess.modifier.addDocument(doc);
647 } catch (IOException e) {
648 throw convertException(e);
649 }
650 //*/
651 //## LUCENE3 ##
652 String query = getQuery(row);
653 Document doc = new Document();
654 doc.add(new Field(LUCENE_FIELD_QUERY, query,
655 Field.Store.YES, Field.Index.NOT_ANALYZED));
656 long time = System.currentTimeMillis();
657 doc.add(new Field(LUCENE_FIELD_MODIFIED,
658 DateTools.timeToString(time, DateTools.Resolution.SECOND),
659 Field.Store.YES, Field.Index.NOT_ANALYZED));
660 StatementBuilder buff = new StatementBuilder();
661 for (int index : indexColumns) {
662 String columnName = columns[index];
663 String data = asString(row[index], columnTypes[index]);
664 // column names that start with _
665 // must be escaped to avoid conflicts
666 // with internal field names (_DATA, _QUERY, _modified)
667 if (columnName.startsWith(LUCENE_FIELD_COLUMN_PREFIX)) {
668 columnName = LUCENE_FIELD_COLUMN_PREFIX + columnName;
669 }
670 doc.add(new Field(columnName, data,
671 Field.Store.NO, Field.Index.ANALYZED));
672 buff.appendExceptFirst(" ");
673 buff.append(data);
674 }
675 Field.Store storeText = STORE_DOCUMENT_TEXT_IN_INDEX ?
676 Field.Store.YES : Field.Store.NO;
677 doc.add(new Field(LUCENE_FIELD_DATA, buff.toString(), storeText,
678 Field.Index.ANALYZED));
679 try {
680 indexAccess.writer.addDocument(doc);
681 if (commitIndex) {
682 commitIndex();
683 }
684 } catch (IOException e) {
685 throw convertException(e);
686 }
687 //*/
688 }
689
690 /**
691 * Delete a row from the index.
692 *
693 * @param row the row
694 * @param commitIndex whether to commit the changes to the Lucene index
695 */
696 protected void delete(Object[] row, boolean commitIndex) throws SQLException {
697 String query = getQuery(row);
698 try {
699 Term term = new Term(LUCENE_FIELD_QUERY, query);
700 /*## LUCENE2 ##
701 indexAccess.modifier.deleteDocuments(term);
702 //*/
703 //## LUCENE3 ##
704 indexAccess.writer.deleteDocuments(term);
705 if (commitIndex) {
706 commitIndex();
707 }
708 //*/
709 } catch (IOException e) {
710 throw convertException(e);
711 }
712 }
713
714 private String getQuery(Object[] row) throws SQLException {
715 StatementBuilder buff = new StatementBuilder();
716 if (schema != null) {
717 buff.append(StringUtils.quoteIdentifier(schema)).append('.');
718 }
719 buff.append(StringUtils.quoteIdentifier(table)).append(" WHERE ");
720 for (int columnIndex : keys) {
721 buff.appendExceptFirst(" AND ");
722 buff.append(StringUtils.quoteIdentifier(columns[columnIndex]));
723 Object o = row[columnIndex];
724 if (o == null) {
725 buff.append(" IS NULL");
726 } else {
727 buff.append('=').append(FullText.quoteSQL(o, columnTypes[columnIndex]));
728 }
729 }
730 return buff.toString();
731 }
732 }
733
734 /**
735 * A wrapper for the Lucene writer and searcher.
736 */
737 static class IndexAccess {
738
739 /**
740 * The index modified.
741 */
742 /*## LUCENE2 ##
743 IndexModifier modifier;
744 //*/
745
746 /**
747 * The index writer.
748 */
749 //## LUCENE3 ##
750 IndexWriter writer;
751 //*/
752
753 /**
754 * The index reader.
755 */
756 //## LUCENE3 ##
757 IndexReader reader;
758 //*/
759
760 /**
761 * The index searcher.
762 */
763 //## LUCENE3 ##
764 Searcher searcher;
765 //*/
766 }
767
768 }
769