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