From 3ffced2c2527a39e785c0ffeb597d95b5a42a07d Mon Sep 17 00:00:00 2001 From: Roman Chyla Date: Thu, 19 May 2011 20:32:43 +0200 Subject: [PATCH] Initial release --- CHANGELOG.txt | 3 + COPYRIGHT.txt | 21 + INSTALL.txt | 210 ++ LICENSE.txt | 340 +++ README | 59 + build.properties.default | 41 + build.xml | 728 +++++++ common-build.xml | 42 + docs/development.txt | 0 docs/hello-world.txt | 0 docs/how-to-wrap.txt | 0 docs/technical-details.txt | 157 ++ examples/README.txt | 6 + examples/invenio/etc/jetty.xml | 212 ++ examples/invenio/etc/logging.properties | 12 + examples/invenio/etc/webdefault.xml | 379 ++++ examples/invenio/solr/conf/admin-extra.html | 31 + .../solr/conf/data-config-test-java.xml | 53 + examples/invenio/solr/conf/data-config.xml | 91 + examples/invenio/solr/conf/elevate.xml | 36 + .../solr/conf/mapping-ISOLatin1Accent.txt | 246 +++ examples/invenio/solr/conf/protwords.txt | 21 + examples/invenio/solr/conf/schema.xml | 666 ++++++ examples/invenio/solr/conf/scripts.conf | 24 + examples/invenio/solr/conf/solrconfig.xml | 1106 ++++++++++ examples/invenio/solr/conf/spellings.txt | 2 + examples/invenio/solr/conf/stopwords.txt | 58 + examples/invenio/solr/conf/synonyms.txt | 31 + examples/invenio/solr/conf/xslt/example.xsl | 132 ++ .../invenio/solr/conf/xslt/example_atom.xsl | 67 + .../invenio/solr/conf/xslt/example_rss.xsl | 66 + examples/invenio/solr/conf/xslt/luke.xsl | 337 +++ examples/invenio/solr/conf/xslt/twitter.xsl | 140 ++ examples/twitter/etc/jetty.xml | 212 ++ examples/twitter/etc/logging.properties | 12 + examples/twitter/etc/webdefault.xml | 379 ++++ examples/twitter/solr/conf/admin-extra.html | 31 + .../solr/conf/data-config-test-java.xml | 53 + examples/twitter/solr/conf/data-config.xml | 91 + examples/twitter/solr/conf/elevate.xml | 36 + .../solr/conf/mapping-ISOLatin1Accent.txt | 246 +++ examples/twitter/solr/conf/protwords.txt | 21 + examples/twitter/solr/conf/schema.xml | 666 ++++++ examples/twitter/solr/conf/scripts.conf | 24 + examples/twitter/solr/conf/solrconfig.xml | 1106 ++++++++++ examples/twitter/solr/conf/spellings.txt | 2 + examples/twitter/solr/conf/stopwords.txt | 58 + examples/twitter/solr/conf/synonyms.txt | 31 + examples/twitter/solr/conf/xslt/example.xsl | 132 ++ .../twitter/solr/conf/xslt/example_atom.xsl | 67 + .../twitter/solr/conf/xslt/example_rss.xsl | 66 + examples/twitter/solr/conf/xslt/luke.xsl | 337 +++ examples/twitter/solr/conf/xslt/twitter.xsl | 140 ++ lib/LICENSE.jzlib.txt | 29 + lib/junit-3.8.2.jar | Bin 0 -> 120640 bytes lib/jzlib-1.0.7.jar | Bin 0 -> 49636 bytes src/java/invenio/montysolr/JettyRunner.java | 147 ++ .../montysolr/JettyRunnerPythonVM.java | 196 ++ src/java/invenio/montysolr/SolrRunner.java | 30 + .../montysolr/examples/TwitterAPIHandler.java | 54 + .../invenio/montysolr/jni/BasicBridge.java | 60 + .../montysolr/jni/MontySolrBridge.java | 63 + .../invenio/montysolr/jni/MontySolrVM.java | 117 + .../invenio/montysolr/jni/PythonBridge.java | 36 + .../invenio/montysolr/jni/PythonMessage.java | 122 ++ .../montysolr/util/DebuggingMethods.java | 25 + .../invenio/montysolr/util/InvenioBitSet.java | 157 ++ src/java/org/ads/solr/InvenioBitSet.java | 73 + .../apache/lucene/queryParser/CharStream.java | 115 + .../queryParser/InvenioQueryParser.java | 1932 +++++++++++++++++ .../lucene/queryParser/InvenioQueryParser.jj | 1493 +++++++++++++ .../InvenioQueryParserConstants.java | 137 ++ .../InvenioQueryParserTokenManager.java | 1372 ++++++++++++ .../lucene/queryParser/ParseException.java | 187 ++ .../apache/lucene/queryParser/QueryParser.jj | 1483 +++++++++++++ .../org/apache/lucene/queryParser/Token.java | 131 ++ .../lucene/queryParser/TokenMgrError.java | 147 ++ .../apache/solr/handler/InvenioHandler.java | 47 + .../solr/handler/PythonDiagnosticHandler.java | 156 ++ .../handler/component/InvenioFormatter.java | 176 ++ .../dataimport/NoRollbackDataImporter.java | 102 + .../dataimport/WaitingDataImportHandler.java | 374 ++++ .../solr/schema/FileResolverTextField.java | 111 + .../apache/solr/schema/PythonTextField.java | 72 + .../org/apache/solr/search/CitationQuery.java | 431 ++++ .../search/CitationRefersToQParserPlugin.java | 317 +++ .../solr/search/InvenioQParserPlugin.java | 543 +++++ .../org/apache/solr/search/InvenioQuery.java | 182 ++ .../solr/search/InvenioQueryBitSet.java | 38 + .../org/apache/solr/search/InvenioWeight.java | 178 ++ .../solr/search/InvenioWeightBitSet.java | 124 ++ .../solr/update/InvenioKeepRecidUpdated.java | 166 ++ .../org/apache/solr/util/DictionaryCache.java | 59 + src/java/org/apache/solr/util/WebUtils.java | 50 + src/python/montysolr/__init__.py | 0 src/python/montysolr/examples/__init__.py | 0 src/python/montysolr/examples/bigtest.py | 79 + src/python/montysolr/examples/twitter_test.py | 62 + src/python/montysolr/handler.py | 182 ++ src/python/montysolr/initvm.py | 45 + src/python/montysolr/inveniopie/__init__.py | 0 src/python/montysolr/inveniopie/api_calls.py | 114 + .../inveniopie/multiprocess_api_calls.py | 127 ++ src/python/montysolr/inveniopie/targets.py | 314 +++ src/python/montysolr/java_bridge.py | 37 + src/python/montysolr/python_bridge.py | 76 + src/python/montysolr/sequential_handler.py | 19 + src/python/montysolr/tests/__init__.py | 0 .../montysolr/tests/run_jetty_servlet.py | 33 + .../montysolr/tests/unittest_run_jetty.py | 28 + src/python/montysolr/utils.py | 14 + src/python/utils/attach_fulltexts.py | 186 ++ src/python/utils/compress_top_folders.py | 28 + src/python/utils/copy_top_folders.py | 31 + src/python/utils/decompress_top_folders.py | 28 + src/python/utils/dump_dicts.py | 27 + src/python/utils/extract_queries.py | 194 ++ src/python/utils/find_fulltexts.py | 159 ++ src/python/utils/harvest_marc.py | 102 + src/python/utils/import_dicts.py | 54 + src/python/utils/run_index.py | 135 ++ .../invenio/montysolr/MontySolrTestCase.java | 96 + .../solr/search/TestInvenioQueryParser.java | 312 +++ test/python/__init__.py | 0 test/python/montysolr_testcase.py | 88 + test/python/run_search.py | 27 + test/python/test_examples_twitter.py | 47 + test/python/test_invenio_queries.py | 55 + test/python/testing_targets.py | 51 + test/python/tmp_run_solr.py | 40 + test/python/unittest_bridge.py | 44 + test/python/unittest_examples_bigtest.py | 214 ++ test/python/unittest_invenio.py | 232 ++ test/python/unittest_python_bridge.py | 70 + test/python/unittest_solr.py | 62 + test/test-files/README | 21 + test/test-files/invenio-test-queries.result | 290 +++ test/test-files/invenio-test-queries.txt | 73 + .../solr/conf/data-config-test-java.xml | 53 + test/test-files/solr/conf/data-config.xml | 91 + test/test-files/solr/conf/elevate.xml | 36 + test/test-files/solr/conf/protwords.txt | 21 + test/test-files/solr/conf/schema.xml | 666 ++++++ test/test-files/solr/conf/solrconfig.xml | 1115 ++++++++++ test/test-files/solr/conf/spellings.txt | 2 + test/test-files/solr/conf/stopwords.txt | 58 + test/test-files/solr/conf/synonyms.txt | 31 + 147 files changed, 26128 insertions(+) create mode 100644 CHANGELOG.txt create mode 100644 COPYRIGHT.txt create mode 100644 INSTALL.txt create mode 100644 LICENSE.txt create mode 100644 README create mode 100644 build.properties.default create mode 100644 build.xml create mode 100644 common-build.xml create mode 100644 docs/development.txt create mode 100644 docs/hello-world.txt create mode 100644 docs/how-to-wrap.txt create mode 100644 docs/technical-details.txt create mode 100644 examples/README.txt create mode 100755 examples/invenio/etc/jetty.xml create mode 100644 examples/invenio/etc/logging.properties create mode 100644 examples/invenio/etc/webdefault.xml create mode 100755 examples/invenio/solr/conf/admin-extra.html create mode 100644 examples/invenio/solr/conf/data-config-test-java.xml create mode 100644 examples/invenio/solr/conf/data-config.xml create mode 100755 examples/invenio/solr/conf/elevate.xml create mode 100755 examples/invenio/solr/conf/mapping-ISOLatin1Accent.txt create mode 100755 examples/invenio/solr/conf/protwords.txt create mode 100755 examples/invenio/solr/conf/schema.xml create mode 100755 examples/invenio/solr/conf/scripts.conf create mode 100755 examples/invenio/solr/conf/solrconfig.xml create mode 100755 examples/invenio/solr/conf/spellings.txt create mode 100755 examples/invenio/solr/conf/stopwords.txt create mode 100755 examples/invenio/solr/conf/synonyms.txt create mode 100755 examples/invenio/solr/conf/xslt/example.xsl create mode 100755 examples/invenio/solr/conf/xslt/example_atom.xsl create mode 100755 examples/invenio/solr/conf/xslt/example_rss.xsl create mode 100755 examples/invenio/solr/conf/xslt/luke.xsl create mode 100755 examples/invenio/solr/conf/xslt/twitter.xsl create mode 100755 examples/twitter/etc/jetty.xml create mode 100644 examples/twitter/etc/logging.properties create mode 100644 examples/twitter/etc/webdefault.xml create mode 100755 examples/twitter/solr/conf/admin-extra.html create mode 100644 examples/twitter/solr/conf/data-config-test-java.xml create mode 100644 examples/twitter/solr/conf/data-config.xml create mode 100755 examples/twitter/solr/conf/elevate.xml create mode 100755 examples/twitter/solr/conf/mapping-ISOLatin1Accent.txt create mode 100755 examples/twitter/solr/conf/protwords.txt create mode 100755 examples/twitter/solr/conf/schema.xml create mode 100755 examples/twitter/solr/conf/scripts.conf create mode 100755 examples/twitter/solr/conf/solrconfig.xml create mode 100755 examples/twitter/solr/conf/spellings.txt create mode 100755 examples/twitter/solr/conf/stopwords.txt create mode 100755 examples/twitter/solr/conf/synonyms.txt create mode 100755 examples/twitter/solr/conf/xslt/example.xsl create mode 100755 examples/twitter/solr/conf/xslt/example_atom.xsl create mode 100755 examples/twitter/solr/conf/xslt/example_rss.xsl create mode 100755 examples/twitter/solr/conf/xslt/luke.xsl create mode 100755 examples/twitter/solr/conf/xslt/twitter.xsl create mode 100644 lib/LICENSE.jzlib.txt create mode 100755 lib/junit-3.8.2.jar create mode 100644 lib/jzlib-1.0.7.jar create mode 100644 src/java/invenio/montysolr/JettyRunner.java create mode 100644 src/java/invenio/montysolr/JettyRunnerPythonVM.java create mode 100644 src/java/invenio/montysolr/SolrRunner.java create mode 100644 src/java/invenio/montysolr/examples/TwitterAPIHandler.java create mode 100644 src/java/invenio/montysolr/jni/BasicBridge.java create mode 100644 src/java/invenio/montysolr/jni/MontySolrBridge.java create mode 100644 src/java/invenio/montysolr/jni/MontySolrVM.java create mode 100644 src/java/invenio/montysolr/jni/PythonBridge.java create mode 100644 src/java/invenio/montysolr/jni/PythonMessage.java create mode 100644 src/java/invenio/montysolr/util/DebuggingMethods.java create mode 100644 src/java/invenio/montysolr/util/InvenioBitSet.java create mode 100755 src/java/org/ads/solr/InvenioBitSet.java create mode 100644 src/java/org/apache/lucene/queryParser/CharStream.java create mode 100644 src/java/org/apache/lucene/queryParser/InvenioQueryParser.java create mode 100644 src/java/org/apache/lucene/queryParser/InvenioQueryParser.jj create mode 100644 src/java/org/apache/lucene/queryParser/InvenioQueryParserConstants.java create mode 100644 src/java/org/apache/lucene/queryParser/InvenioQueryParserTokenManager.java create mode 100644 src/java/org/apache/lucene/queryParser/ParseException.java create mode 100755 src/java/org/apache/lucene/queryParser/QueryParser.jj create mode 100644 src/java/org/apache/lucene/queryParser/Token.java create mode 100644 src/java/org/apache/lucene/queryParser/TokenMgrError.java create mode 100644 src/java/org/apache/solr/handler/InvenioHandler.java create mode 100644 src/java/org/apache/solr/handler/PythonDiagnosticHandler.java create mode 100644 src/java/org/apache/solr/handler/component/InvenioFormatter.java create mode 100644 src/java/org/apache/solr/handler/dataimport/NoRollbackDataImporter.java create mode 100644 src/java/org/apache/solr/handler/dataimport/WaitingDataImportHandler.java create mode 100644 src/java/org/apache/solr/schema/FileResolverTextField.java create mode 100644 src/java/org/apache/solr/schema/PythonTextField.java create mode 100644 src/java/org/apache/solr/search/CitationQuery.java create mode 100644 src/java/org/apache/solr/search/CitationRefersToQParserPlugin.java create mode 100644 src/java/org/apache/solr/search/InvenioQParserPlugin.java create mode 100644 src/java/org/apache/solr/search/InvenioQuery.java create mode 100644 src/java/org/apache/solr/search/InvenioQueryBitSet.java create mode 100644 src/java/org/apache/solr/search/InvenioWeight.java create mode 100644 src/java/org/apache/solr/search/InvenioWeightBitSet.java create mode 100644 src/java/org/apache/solr/update/InvenioKeepRecidUpdated.java create mode 100644 src/java/org/apache/solr/util/DictionaryCache.java create mode 100644 src/java/org/apache/solr/util/WebUtils.java create mode 100644 src/python/montysolr/__init__.py create mode 100644 src/python/montysolr/examples/__init__.py create mode 100644 src/python/montysolr/examples/bigtest.py create mode 100644 src/python/montysolr/examples/twitter_test.py create mode 100644 src/python/montysolr/handler.py create mode 100644 src/python/montysolr/initvm.py create mode 100644 src/python/montysolr/inveniopie/__init__.py create mode 100644 src/python/montysolr/inveniopie/api_calls.py create mode 100644 src/python/montysolr/inveniopie/multiprocess_api_calls.py create mode 100644 src/python/montysolr/inveniopie/targets.py create mode 100644 src/python/montysolr/java_bridge.py create mode 100644 src/python/montysolr/python_bridge.py create mode 100644 src/python/montysolr/sequential_handler.py create mode 100644 src/python/montysolr/tests/__init__.py create mode 100644 src/python/montysolr/tests/run_jetty_servlet.py create mode 100644 src/python/montysolr/tests/unittest_run_jetty.py create mode 100644 src/python/montysolr/utils.py create mode 100644 src/python/utils/attach_fulltexts.py create mode 100644 src/python/utils/compress_top_folders.py create mode 100644 src/python/utils/copy_top_folders.py create mode 100644 src/python/utils/decompress_top_folders.py create mode 100644 src/python/utils/dump_dicts.py create mode 100644 src/python/utils/extract_queries.py create mode 100644 src/python/utils/find_fulltexts.py create mode 100644 src/python/utils/harvest_marc.py create mode 100644 src/python/utils/import_dicts.py create mode 100644 src/python/utils/run_index.py create mode 100644 test/java/invenio/montysolr/MontySolrTestCase.java create mode 100644 test/java/org/apache/solr/search/TestInvenioQueryParser.java create mode 100644 test/python/__init__.py create mode 100644 test/python/montysolr_testcase.py create mode 100644 test/python/run_search.py create mode 100644 test/python/test_examples_twitter.py create mode 100644 test/python/test_invenio_queries.py create mode 100644 test/python/testing_targets.py create mode 100644 test/python/tmp_run_solr.py create mode 100644 test/python/unittest_bridge.py create mode 100644 test/python/unittest_examples_bigtest.py create mode 100644 test/python/unittest_invenio.py create mode 100644 test/python/unittest_python_bridge.py create mode 100644 test/python/unittest_solr.py create mode 100644 test/test-files/README create mode 100644 test/test-files/invenio-test-queries.result create mode 100644 test/test-files/invenio-test-queries.txt create mode 100644 test/test-files/solr/conf/data-config-test-java.xml create mode 100644 test/test-files/solr/conf/data-config.xml create mode 100755 test/test-files/solr/conf/elevate.xml create mode 100755 test/test-files/solr/conf/protwords.txt create mode 100755 test/test-files/solr/conf/schema.xml create mode 100755 test/test-files/solr/conf/solrconfig.xml create mode 100755 test/test-files/solr/conf/spellings.txt create mode 100755 test/test-files/solr/conf/stopwords.txt create mode 100755 test/test-files/solr/conf/synonyms.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 000000000..ce36b1fa8 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,3 @@ +MontySolr 0.1, 2011-05-19 +------------------------- +- Initial release \ No newline at end of file diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt new file mode 100644 index 000000000..a46d0c12b --- /dev/null +++ b/COPYRIGHT.txt @@ -0,0 +1,21 @@ +All MontySolr code 2011 is copyright by the original authors. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or (at +your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with this program as the file LICENSE.txt; if not, please see +http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + +Drupal includes works under other copyright notices and distributed +according to the terms of the GNU General Public License or a compatible +license, including: + + jzlib - Copyright (c) 2000-2003 ymnk, JCraft,Inc. \ No newline at end of file diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 000000000..e5a1ed8cb --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,210 @@ + + +For the impatient: + + $ cd montysolr + $ cp build.properties.default build.properties + $ vim build.properties # review the config and set the correct paths + $ ant automatic-install + + +HOWEVER: this will work only if you already have pre-requisities installed OR if we +built them for you. In any case, give it a try! + +The automatic installation will: + + * check correct version of Python + * check for JCC module (if available in our repository, we will offer to download and install it for you) + * check pylucene package (again, we'll download and install it if available) + * build the wrapper for Solr (as a Python module) + * build MontySolr (as a Python module) + * run some basic tests + +== STANDARD INSTALLATION == + +To build MontySolr you need: + + * Java JDK >= 1.6 (tested and worked with both OpenJDK and Sun Java JDK) + * ant >= 1.6 + * Python >= 2.4 (if you want to take advantage of multiprocessing, then at least Python 2.5) + * JCC module for Python + * PyLucene + * setuptools for Python (needed for installation of pylucene) + + +The recommended installation is this: + + * Install JCC + - http://lucene.apache.org/pylucene/documentation/install.html + - NOTE: you must build JCC in a shared mode (default now) + + {{{ + export JCC_LFLAGS='-framework:JavaVM:-framework:Python' + python setup.py build + python setup.py install + }}} + + + $ cp build.properties.default build.properties + $ vim build.properties # edit and review the configuration + $ ant assemble-example # assemble demo example + $ ant montysolr-build # compile montysolr + $ ant run-montysolr # run the demo + + + + + + + + + + + + * Follow the installation instructions for JCC at http://pypi.python.org/pypi/JCC/ + +1. create a copy of the build.properties + + + +=== JCC NOTES === + +http://lucene.apache.org/pylucene/jcc/index.html + +JCC is a code generator by Andi Vajda. It is used to wrap Java into a tiny layer of C++. Thanks to the work +of JCC we can build Python modules from the Java code. Thanks to JCC we can use Java inside Python, and also +Python inside Java! + +JCC must be built in a shared mode (default now). To check it, you can do: + + $ python -c "from jcc import config; print 'version=', config.VERSION, ', shared=', config.SHARED" + +If shared is not 'True', then you have to rebuilt JCC + + $ cd /some/dir/with/jcc + $ export USE_DISTUTILS + $ python setup.py build + $ python setup.py install + + -- if you are on Mac OS X, the sequence is: -- + + $ cd /some/dir/with/jcc + $ export JCC_LFLAGS='-framework:JavaVM:-framework:Python' + $ export USE_DISTUTILS + $ python setup.py build + $ python setup.py install + +If your system does not have the correct setup, JCC will warn you and will also provide the instructions on +how to fix it. + +-- Note for Mac OS X users -- + + +=== PYLUCENE === + +http://lucene.apache.org/pylucene + +JCC builds PyLucene project which is simply a normal Lucene wrapped into a Python package. + +Because we don't want to duplicate code, MontySolr takes advantage of pylucene built as a separate module. +For this to work Pylucene, solr and also montysolr packages must be built in a shared mode. +Shared mode is default on many recent systems now, but it is good to check that. + + TODO: build a small program that checks shared mode + +In MontySolr we use generics support for Java (this makes your life as Java programmers much easier +in Python). Unfortunately, generics is not yet a default option. So if you already built a pylucene, +you will have to rebuild it again (in my experience, inclusion of generics does not have a negative +impact -- besides you having to work with newer versions of Java, but you do that already, right?) + +So the build of Lucene is: + + $ set JCCFLAGS= + $ export JCCFLAGS + $ make + $ make install + + + +== FAQ == + +Q: What version of Solr shall I user? + +You can use both Solr 1.4 and 3.x but make sure that also your PyLucene is the same version, *including minor versions*! +Ie. if your solr is using 2.9.3, then also your pylucene must have 2.9.3. + + + +Q: Do I need a separate distrubution of the lucene sources or is what's in the solr distribution enough? + +It is enough. But PyLucene has the lucene jars inside, so inevitably you will end up with two sets +of lucene jars. This is not a problem though. Just make sure that your PyLucene is using the same +version as your Solr instance! + + + + +Q: When I start montysolr, I see errors "ImportError: No module named solr_java" ... or "lucene", "foo", "bar" etc. + +The message comes from the Python interpreter that cannot find some module. If you installed lucene or +any other into non-standard location, then you have to make that location known to Python. + + * use PYTHONPATH + - e.g. "export PYTHONPATH=/some/path:/some/other/path" + * if you start montysolr with ant ("ant run-montysolr") + - edit build.properties + - python_path=/some/path:/some/other/path + +If the missing module is "solr_java" then you did not finish installation properly, you can fix it by: + + $ ant solr-build + +You shall find the solr_java module inside "./montysolr/build/dist" -- from there it can be easily installed. + + $ cd ./montysolr/build/dist + $ easy_install solr_java-0.1-py2.6-macosx-10.6-universal.egg + + + + +Q: When building the montysolr, I get these errors: + + build/_solr_java/__wrap__.cpp: In function ‘PyObject* org::apache::solr::search::function::t_DualFloatFunction_createWeight(org::apache::solr::search::function::t_DualFloatFunction*, PyObject*)’: + build/_solr_java/__wrap__.cpp:2325: error: ‘parameters_’ is not a member of ‘java::util::t_Map’ + +Your PyLucene is not built with the generics support. Please see above how to fix it. + + + +Q: I am using python virtualenv, will it cause problems? + +Absolutely no. + +Q: Are there any limitations of what I can run inside the Python? + +No, if it works in the normal Python session, it will work also inside Solr (provided that you set up correct paths, +have enough memory to host both systems etc.) + + +Q: I have several versions of Python installed, how to run MontySolr with the non-standard one? + +In the past, I was doing something like this: + +I was doing this: + +# first set up environment vars that make system use different Python + +$ export LD_LIBRARY_PATH=/opt/rchyla/python26/lib/:/opt/rchyla/python26/lib/python2.6/lib-dynload/ +$ export PYTHONHOME=/opt/rchyla/python26/ +$ export PYTHONPATH=/opt/rchyla/workspace/:/opt/rchyla/workspace/solrpie/python/ +$ export PATH=/opt/rchyla/python26/bin:/opt/rchyla/python26:/afs/cern.ch/user/r/rchyla/public/jdk1.6.0_18/bin/:$PATH + +# then use the ant to run MontySolr as a daemon + +$ export SOLRPIE_ARGS='--port 8443 --daemon' +$ export SOLRPIE_JVMARGS='-d64 -Xmx2048m -Dsolrpie.max_workers=5 -Dsolrpie.max_threads=200' +$ export SOLRPIE_MAX_WORKERS=5 +$ export SOLRPIE_NEWENVIRONMENT=false +$ ant run-solrpie-daemon + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..5b6e7c66c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README b/README new file mode 100644 index 000000000..ff9b64155 --- /dev/null +++ b/README @@ -0,0 +1,59 @@ +CONTENTS OF THIS FILE +--------------------- + +* About MontySolr +* Configuration and features +* Developing for MontySolr + +ABOUT MONTYSOLR +------------ + +MontySolr is an open source extension that makes it possible to include Python +code inside Solr (http://lucene.apache.org/solr). You can call Python routines +from the Java side, as well as control (most of the) Solr operations from the +Python side. + + +CONFIGURATION AND FEATURES +-------------------------- + +MontySolr (what you get when you download and extract montysolr-x.y.tgz) is only +an extension for Solr. You will need a separate Solr instance as well as a few +dependencies to use MontySolr. + + +More about configuration: + * Install, upgrade, and maintaince: + See INSTALL.txt in the same directory as this document. + * Learn about how to extend MontySolr: + See docs/technical-details.txt + * See also: https://svnweb.cern.ch/trac/rcarepo/wiki/MontySolr + + +DEVELOPING FOR MONTYSOLR +------------------------ + +MontySolr contains very simple API and the layer between Solr and Python is +intentionally kept minimal. In most cases you simply want to use MontySolr just +as a communication layer between Solr and your own Python-written system. In this +case you don't need to make any changes inside MontySolr, but you will write simple +Python code that controls the business logic between Solr and Python. + +More about writing wrappers to call your Python system(s): + * Hello world example + See docs/hello-world.txt + * To understand details of the wrappers + See docs/how-to-wrap.txt + + + +If you need new functionality that is not present in MontySolr, search for +existing solutions or discussion on the mailing list: + + * Invenio Development Team + + + + * For more information about developing + See docs/development.txt + diff --git a/build.properties.default b/build.properties.default new file mode 100644 index 000000000..99d0cc46a --- /dev/null +++ b/build.properties.default @@ -0,0 +1,41 @@ + +# These are the main variables you may need to change for successful build +# of montysolr modules. The more detailed settings can be changed in the configuration +# section of the build.xml file. Be careful to remove trailing whitespaces. + +# based on the installed version of JCC, you have to select the +# correct invocation + + +jcc=jcc.__main__ + +# folder where the solr lives +solr_home=/x/dev/workspace/apache-solr-1.4.1 + +# your custom solr configuration +webdist=./example + +# python executable +python=python + +# PYTHONPATH : the tasks with the name of run-* (eg. run-solr) +# are starting JVM and the JVM has a Python inside. This python +# interpreter needs to know where to look for python packages +# By default, run-* tasks add ./python and ./build/dist folders +# to the classpath, but here you can add or change the, this +# PYTHONPATH will be prepended + +python_path=/opt/invenio/lib/python + +# Needed only if you want to regenerate InvenioQueryParser syntax, +# which is probably not what you need to do + +javacc.home=/some/path/javacc-5.0 diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..d394f2481 --- /dev/null +++ b/build.xml @@ -0,0 +1,728 @@ + + + Java extensions for Invenio - Java search engine made python-friendly + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The properties of the project are not set correctly. Copy "build.properties.default" -> "build.properties" and edit the new file if necessary. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${toString:montysolr.classpath} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Running montysolr as: +======== +java -cp '${jcc_egg}/jcc/classes${path.separator}${classes.dir}${path.separator}${toString:montysolr.classpath}' + -Dsolr.solr.home=${webdist.home}/solr -Dsolr.data.dir=${webdist.home}/solr/data + -Djava.library.path=${jcc_egg} + ${env.MONTYSOLR_JVMARGS} + --webroot ${webdist.webapp} + --context /solr + ${env.MONTYSOLR_ARGS} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One or more of the JavaCC .jj files is newer than its corresponding + .java file. Run the "javacc" target to regenerate the artifacts. + + + + + + ################################################################## + JavaCC not found. + JavaCC Home: ${javacc.home} + JavaCC JAR: ${javacc.jar} + + Please download and install JavaCC from: + + <http://javacc.dev.java.net> + + Then, create a build.properties file either in your home + directory, or within the Lucene directory and set the javacc.home + property to the path where JavaCC is installed. For example, + if you installed JavaCC in /usr/local/java/javacc-3.2, then set the + javacc.home property to: + + javacc.home=/usr/local/java/javacc-3.2 + + If you get an error like the one below, then you have not installed + things correctly. Please check all your paths and try again. + + java.lang.NoClassDefFoundError: org.javacc.parser.Main + ################################################################## + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The MontySolr example was assembled from the original Solr example + (${solr.home}/example) + See ${montysolr.home}/examples/README.txt for instructions on how to + run the demos. + + + + + Downloading solr ${solr.version} from ${solr.url} + + + + + Building the Solr example + + + + + + + + + + + + + + diff --git a/common-build.xml b/common-build.xml new file mode 100644 index 000000000..8c35de8c6 --- /dev/null +++ b/common-build.xml @@ -0,0 +1,42 @@ + + + + + + This file is designed for importing into a main build file, and not intended + for standalone use. + + + + + + + + + @{dest} + + + + + + + + + diff --git a/docs/development.txt b/docs/development.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/hello-world.txt b/docs/hello-world.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/how-to-wrap.txt b/docs/how-to-wrap.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/technical-details.txt b/docs/technical-details.txt new file mode 100644 index 000000000..09b53394b --- /dev/null +++ b/docs/technical-details.txt @@ -0,0 +1,157 @@ += Technical Details of the Solr-Python-Invenio integration = + +== Embedding Solr == + +There are three ways that I found/considered to embed Solr: + + * org.apache.solr.client.solrj.embedded.EmbeddedSolrServer + - this is the default way, Solr is running as an embedded process, not inside a servlet container + - the parent process is responsible for querying Solr using the Solr API + - this is implemented in the rca.solr.GetRecIds + * org.apache.solr.servlet.DirectSolrConnect + - this is like the above, but easier -- all the queries are sent as strings, everything is just a string. + This solution is very flexible and probably suitable for quick integration + * embed servlet container (in this case Jetty) + - this is the craziest, but potentially the most powerful solution (so I went for it) + - we encapsulate the servlet container and let it run Solr as normally, everything is just as it was in the normal server appliance + - test implemented in the rca.solr.JettyRunner + * embed Python VM in Java VM + - well, this is the best (not sure if crazy) solution which Andi showed me after I asked + - it embeds PythonVM inside Java and therefore it is more straightforward, possible cleaner than communication from python with jetty + Because there i would have to devise ways how to initiate calls to python from Java + + + + +== Servlet Jetty Embedding == + + +OK, so I had to discover those things (and who knows what else will have to be discovered): + + - classloaders and servlets + - normally there are 3 class loaders in java (system, application and one another which i forgot) + - class loaders ask their parents before loading the new classes + - HOWEVER, for servlets this is not true; servlet classloader can ignore (and actually should) ignore + the already loaded class (of its parent class loaders) -- well, jetty creates a new classloader + that does not have a parent classloader + - THEREFORE - my singletons were not absolute singletons + - There are two ways to solve this: + - tell Jetty to behave (like normal java) (i use this) + - register parent classloader + + - extra classes + - it is important, that the montysolr singleton classes ARE NOT present in the webapp/WEB-INF + - anything there might be loaded by the classloader separately, even if we specified classpath + or configured jetty's classloader + - JUST KEEP montysolr classes OUT OF webapp! + + +=== Good Practice === + - keep them separated, what belongs to Solr should live inside solr + - what belongs to Jetty, lives inside webapp + - our code is starting Jetty, Jetty reads Solr, Solr will load our classes, BUT we don't + include things neither in solr nor in webapps folders! + + - HOWEVER, certain special features must be activated in the solr configuration - so WE EDIT + solrconfig.xml + + + +=== TODO === + - ~~run JettyRunner in Python~~ + - organize imports (look at solr distro -- jars from solr/dist should be included) + - fix build.xml + - can we wrap jetty start.jar? -- to do the hard job of setting classpath, but also make sure we can load our singleton? + + +== Embed PythonVM == + + - JCC must be built in a shared mode (default) + - however, on Mac, LCFLAGS must also contain 'framework Python', otherwise you get error when trying to start PythonVM + {{{ + System.loadLibrary("jcc"); + Exception in thread "main" java.lang.UnsatisfiedLinkError: + /Library/Python/2.6/site-packages/JCC-2.6-py2.6-macosx-10.6-universal.egg/libjcc.dylib: + Symbol not found: _PyExc_RuntimeError + +Andi's explanation: +That's because Python's shared library wasn't found. The reason is that, by default, Python's shared lib not on JCC's link line because normally JCC is loaded into a Python process and the dynamic linker thus finds the symbols needed inside the process. + +Here, since you're not starting inside a Python process, you need to add '-framework Python' to JCC's LFLAGS in setup.py so that the dynamic linker can find the Python VM shared lib and load it. + + }}} + - so change the JCC setup.py, or add LFLAGS and rebuild + {{{ + export JCC_LFLAGS='-framework:JavaVM:-framework:Python' + python setup.py build + python setup.py install + }}} + + - the extension that i build with JCC is also runnable from inside Python, therefore I can built one extension, and run + java from python, or python from java + - to do that, I have to be careful to compile wrapper only for chosen classes + - do not use --jar montysolr.jar for compilation + - or make sure, that some classes (namely org.apache.jcc.PythonVM) are not called from my public classes + - because during build, jcc compiler will try to load the extension which we are just compiling + - JCC does not like singletons (neither on Python side, nor on Java side - it was hanging at vm.acquireThreadState()) + - it is possible to use Interfaces, however the wrapped java class must have the basic methods in itself (not inherited; i tried that, + even with protected fields, it was throwing error unsatisfied link error) + - be careful to exclude *from solr* classes that you build for your own extension (jcc will ignore them if they were already built, then + i was getting misterious unsatisfied link error because they were included from solr but i didn't know that) + + - by default, lucene is build with --no-generics (and also i was building other modules without generics, however that makes + it difficult for writing things in Python and pass to Java. Therefore it is necessary to have modules built with generics. + MontySolr builds with generics by default, but if lucene was not build like that, we cannot probably shared them. + + So to rectify that, build lucene with generics (empty JCCFLAGS switches that off) - worked fine on Mac 10.6 w/ Python 2.6 + and WindowsXP w/ Python2.5 + {{{ + export JCCFLAGS= + make + make install + }}} + + +== Programming for Java == + + 1. Watch out for invisible differences between Python and Java wrapped objects + dictname = String('dictname') + dictname.__hash__() + 1926449949 + s = 'dictname' + s.__hash__() + 1024421145 + + Because Python entries in the dictionary are recognized by __hash__, this will + not find anything + d = {'dictname': 1} + d[dictname] + + However, this will work + d[str(dictname)] + + 2. Printing big objects will crash the VM (if there is not enough memory) + + 3. If running invenio, and using MySQLdb extension, or basically any other + extensions - the java must be in the compatible mode - which is usually 32bit + java -d32 .... + + 4. Invalid access memory error.... + + This error is caused by two things: either you try to access java native method that + does not exist. JCC then helps you to find out by dying immediately. + + The other problem is actually more tricky: it is the intbitset module of Invenio. + This module is written in C and in case of error, it prints no diagnostic messages. + And the error inside the C extension brings down the whole JavaVM. To find out this + error, run unittests for python. + + + +== TO PUT SOMEWHERE == + + start JettyRunner with -Dsolr.data.dir=/some/path/to/solr/data + - otherwise solr creates an empty index in ./solr/data + - this is configurable in solr/conf/solrconfig.xml + + \ No newline at end of file diff --git a/examples/README.txt b/examples/README.txt new file mode 100644 index 000000000..46306e30f --- /dev/null +++ b/examples/README.txt @@ -0,0 +1,6 @@ +This folder holds the configuration files of the various Solr examples/demos. +The examples contain only the changed files, please run the following commands +to assemble the full structure: + + $ cd montysolr + $ ant examples-assemble \ No newline at end of file diff --git a/examples/invenio/etc/jetty.xml b/examples/invenio/etc/jetty.xml new file mode 100755 index 000000000..ebf3eebde --- /dev/null +++ b/examples/invenio/etc/jetty.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + org.mortbay.jetty.Request.maxFormContentSize + 1000000 + + + + + + + + + 10 + 50 + 60 + + + + + + + + + + + + + + + + + + + + + + 50000 + 1500 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /contexts + 1 + + + + + + + + + + + + + + + + + + + + + + /webapps + false + true + false + /etc/webdefault.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + /yyyy_mm_dd.request.log + 90 + true + false + GMT + + + + + + + + true + + true + + + diff --git a/examples/invenio/etc/logging.properties b/examples/invenio/etc/logging.properties new file mode 100644 index 000000000..6a54f49d7 --- /dev/null +++ b/examples/invenio/etc/logging.properties @@ -0,0 +1,12 @@ +# Default global logging level: +.level= SEVERE + +# Write to a file: +handlers= java.util.logging.FileHandler + +# Write log messages in XML format: +#java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter +java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter + +# Log to the current working directory, with log files named solrxxx.log +java.util.logging.FileHandler.pattern = /tmp/solr-test-%u.log \ No newline at end of file diff --git a/examples/invenio/etc/webdefault.xml b/examples/invenio/etc/webdefault.xml new file mode 100644 index 000000000..66bbdd5f8 --- /dev/null +++ b/examples/invenio/etc/webdefault.xml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + Default web.xml file. + This file is applied to a Web application before it's own WEB_INF/web.xml file + + + + + + + + + + org.mortbay.jetty.webapp.NoTLDJarPattern + start.jar|ant-.*\.jar|dojo-.*\.jar|jetty-.*\.jar|jsp-api-.*\.jar|junit-.*\.jar|servlet-api-.*\.jar|dnsns\.jar|rt\.jar|jsse\.jar|tools\.jar|sunpkcs11\.jar|sunjce_provider\.jar|xerces.*\.jar| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + org.mortbay.jetty.servlet.DefaultServlet + + acceptRanges + true + + + dirAllowed + true + + + redirectWelcome + false + + + maxCacheSize + 2000000 + + + maxCachedFileSize + 254000 + + + maxCachedFiles + 1000 + + + gzip + false + + + useFileMappedBuffer + false + + + 0 + + + default / + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + logVerbosityLevel + WARNING + + + fork + false + + + xpoweredBy + false + + + 0 + + + + jsp + *.jsp + *.jspf + *.jspx + *.xsp + *.JSP + *.JSPF + *.JSPX + *.XSP + + + + + + + + + + + + + + + + + + + + + + + + invoker + org.mortbay.jetty.servlet.Invoker + + verbose + true + + + nonContextServlets + true + + + dynamicParam + anyValue + + 1 + + + invoker /servlet/* + + + + + + + 30 + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + + + arISO-8859-6 + beISO-8859-5 + bgISO-8859-5 + caISO-8859-1 + csISO-8859-2 + daISO-8859-1 + deISO-8859-1 + elISO-8859-7 + enISO-8859-1 + esISO-8859-1 + etISO-8859-1 + fiISO-8859-1 + frISO-8859-1 + hrISO-8859-2 + huISO-8859-2 + isISO-8859-1 + itISO-8859-1 + iwISO-8859-8 + jaShift_JIS + koEUC-KR + ltISO-8859-2 + lvISO-8859-2 + mkISO-8859-5 + nlISO-8859-1 + noISO-8859-1 + plISO-8859-2 + ptISO-8859-1 + roISO-8859-2 + ruISO-8859-5 + shISO-8859-5 + skISO-8859-2 + slISO-8859-2 + sqISO-8859-2 + srISO-8859-5 + svISO-8859-1 + trISO-8859-9 + ukISO-8859-5 + zhGB2312 + zh_TWBig5 + + + + + + diff --git a/examples/invenio/solr/conf/admin-extra.html b/examples/invenio/solr/conf/admin-extra.html new file mode 100755 index 000000000..aa739da86 --- /dev/null +++ b/examples/invenio/solr/conf/admin-extra.html @@ -0,0 +1,31 @@ + + + diff --git a/examples/invenio/solr/conf/data-config-test-java.xml b/examples/invenio/solr/conf/data-config-test-java.xml new file mode 100644 index 000000000..3dec8773e --- /dev/null +++ b/examples/invenio/solr/conf/data-config-test-java.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/invenio/solr/conf/data-config.xml b/examples/invenio/solr/conf/data-config.xml new file mode 100644 index 000000000..3a0080d5a --- /dev/null +++ b/examples/invenio/solr/conf/data-config.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/invenio/solr/conf/elevate.xml b/examples/invenio/solr/conf/elevate.xml new file mode 100755 index 000000000..9b4caec69 --- /dev/null +++ b/examples/invenio/solr/conf/elevate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/examples/invenio/solr/conf/mapping-ISOLatin1Accent.txt b/examples/invenio/solr/conf/mapping-ISOLatin1Accent.txt new file mode 100755 index 000000000..ede774258 --- /dev/null +++ b/examples/invenio/solr/conf/mapping-ISOLatin1Accent.txt @@ -0,0 +1,246 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Syntax: +# "source" => "target" +# "source".length() > 0 (source cannot be empty.) +# "target".length() >= 0 (target can be empty.) + +# example: +# "À" => "A" +# "\u00C0" => "A" +# "\u00C0" => "\u0041" +# "ß" => "ss" +# "\t" => " " +# "\n" => "" + +# À => A +"\u00C0" => "A" + +# Á => A +"\u00C1" => "A" + +#  => A +"\u00C2" => "A" + +# à => A +"\u00C3" => "A" + +# Ä => A +"\u00C4" => "A" + +# Å => A +"\u00C5" => "A" + +# Æ => AE +"\u00C6" => "AE" + +# Ç => C +"\u00C7" => "C" + +# È => E +"\u00C8" => "E" + +# É => E +"\u00C9" => "E" + +# Ê => E +"\u00CA" => "E" + +# Ë => E +"\u00CB" => "E" + +# Ì => I +"\u00CC" => "I" + +# Í => I +"\u00CD" => "I" + +# Î => I +"\u00CE" => "I" + +# Ï => I +"\u00CF" => "I" + +# IJ => IJ +"\u0132" => "IJ" + +# Ð => D +"\u00D0" => "D" + +# Ñ => N +"\u00D1" => "N" + +# Ò => O +"\u00D2" => "O" + +# Ó => O +"\u00D3" => "O" + +# Ô => O +"\u00D4" => "O" + +# Õ => O +"\u00D5" => "O" + +# Ö => O +"\u00D6" => "O" + +# Ø => O +"\u00D8" => "O" + +# Œ => OE +"\u0152" => "OE" + +# Þ +"\u00DE" => "TH" + +# Ù => U +"\u00D9" => "U" + +# Ú => U +"\u00DA" => "U" + +# Û => U +"\u00DB" => "U" + +# Ü => U +"\u00DC" => "U" + +# Ý => Y +"\u00DD" => "Y" + +# Ÿ => Y +"\u0178" => "Y" + +# à => a +"\u00E0" => "a" + +# á => a +"\u00E1" => "a" + +# â => a +"\u00E2" => "a" + +# ã => a +"\u00E3" => "a" + +# ä => a +"\u00E4" => "a" + +# å => a +"\u00E5" => "a" + +# æ => ae +"\u00E6" => "ae" + +# ç => c +"\u00E7" => "c" + +# è => e +"\u00E8" => "e" + +# é => e +"\u00E9" => "e" + +# ê => e +"\u00EA" => "e" + +# ë => e +"\u00EB" => "e" + +# ì => i +"\u00EC" => "i" + +# í => i +"\u00ED" => "i" + +# î => i +"\u00EE" => "i" + +# ï => i +"\u00EF" => "i" + +# ij => ij +"\u0133" => "ij" + +# ð => d +"\u00F0" => "d" + +# ñ => n +"\u00F1" => "n" + +# ò => o +"\u00F2" => "o" + +# ó => o +"\u00F3" => "o" + +# ô => o +"\u00F4" => "o" + +# õ => o +"\u00F5" => "o" + +# ö => o +"\u00F6" => "o" + +# ø => o +"\u00F8" => "o" + +# œ => oe +"\u0153" => "oe" + +# ß => ss +"\u00DF" => "ss" + +# þ => th +"\u00FE" => "th" + +# ù => u +"\u00F9" => "u" + +# ú => u +"\u00FA" => "u" + +# û => u +"\u00FB" => "u" + +# ü => u +"\u00FC" => "u" + +# ý => y +"\u00FD" => "y" + +# ÿ => y +"\u00FF" => "y" + +# ff => ff +"\uFB00" => "ff" + +# fi => fi +"\uFB01" => "fi" + +# fl => fl +"\uFB02" => "fl" + +# ffi => ffi +"\uFB03" => "ffi" + +# ffl => ffl +"\uFB04" => "ffl" + +# ſt => ft +"\uFB05" => "ft" + +# st => st +"\uFB06" => "st" diff --git a/examples/invenio/solr/conf/protwords.txt b/examples/invenio/solr/conf/protwords.txt new file mode 100755 index 000000000..1dfc0abec --- /dev/null +++ b/examples/invenio/solr/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/examples/invenio/solr/conf/schema.xml b/examples/invenio/solr/conf/schema.xml new file mode 100755 index 000000000..3b63345be --- /dev/null +++ b/examples/invenio/solr/conf/schema.xml @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/invenio/solr/conf/scripts.conf b/examples/invenio/solr/conf/scripts.conf new file mode 100755 index 000000000..f58b262ae --- /dev/null +++ b/examples/invenio/solr/conf/scripts.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +user= +solr_hostname=localhost +solr_port=8983 +rsyncd_port=18983 +data_dir= +webapp_name=solr +master_host= +master_data_dir= +master_status_dir= diff --git a/examples/invenio/solr/conf/solrconfig.xml b/examples/invenio/solr/conf/solrconfig.xml new file mode 100755 index 000000000..66ebbbdf2 --- /dev/null +++ b/examples/invenio/solr/conf/solrconfig.xml @@ -0,0 +1,1106 @@ + + + + + + ${solr.abortOnConfigurationError:true} + + + + + + + + + + + + + + + + ${solr.data.dir:./solr/data} + + + + + + false + + 10 + + + + + 32 + + 10000 + 1000 + 10000 + + + + + + + + + + + + + native + + + + + + + false + 32 + 10 + + + + + + + + false + + + true + + + + + + + + 1 + + 0 + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + true + + + + + + + + 20 + + + 200 + + + + + + + + + + + + + solr rocks010 + static firstSearcher warming query from solrconfig.xml + {!iq iq.mode=maxinv}refersto:recid:100010invenio + {!iq iq.mode=maxinv}citedby:recid:100010invenio + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + explicit + + + + + + + + + + + query + invenio-formatter + facet + mlt + highlight + stats + debug + + + + iq + explicit + invenio + maxinv + fulltext + AND + + + + + + + + + + + + + dismax + explicit + 0.01 + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + + + text^0.2 features^1.1 name^1.5 manu^1.4 manu_exact^1.9 + + + popularity^0.5 recip(price,1,1000,1000)^0.3 + + + id,name,price,score + + + 2<-1 5<-2 6<90% + + 100 + *:* + + text features name + + 0 + + name + regex + + + + + + + dismax + explicit + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 + 2<-1 5<-2 6<90% + + incubationdate_dt:[* TO NOW/DAY-1MONTH]^2.2 + + + + inStock:true + + + + cat + manu_exact + price:[* TO 500] + price:[500 TO *] + + + + + + + + + + textSpell + + + default + name + ./spellchecker + + + + + + + + + + + + + + + + false + + false + + 1 + + + spellcheck + + + + + + + + true + + + tvComponent + + + + + + + + + default + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + 20 + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + true + default + true + + name + id + + features + + true + + + + false + + + clusteringComponent + + + + + + + + text + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + true + + + termsComponent + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + standard + solrpingquery + all + + + + + + + explicit + true + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + solr + + + + + + + + + data-config.xml + false + false + + + + + data-config.xml + false + false + + + + + data-config-test-java.xml + false + false + + + + + + diff --git a/examples/invenio/solr/conf/spellings.txt b/examples/invenio/solr/conf/spellings.txt new file mode 100755 index 000000000..d7ede6f56 --- /dev/null +++ b/examples/invenio/solr/conf/spellings.txt @@ -0,0 +1,2 @@ +pizza +history \ No newline at end of file diff --git a/examples/invenio/solr/conf/stopwords.txt b/examples/invenio/solr/conf/stopwords.txt new file mode 100755 index 000000000..b5824da32 --- /dev/null +++ b/examples/invenio/solr/conf/stopwords.txt @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +#Standard english stop words taken from Lucene's StopAnalyzer +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with + diff --git a/examples/invenio/solr/conf/synonyms.txt b/examples/invenio/solr/conf/synonyms.txt new file mode 100755 index 000000000..b0e31cb7e --- /dev/null +++ b/examples/invenio/solr/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + diff --git a/examples/invenio/solr/conf/xslt/example.xsl b/examples/invenio/solr/conf/xslt/example.xsl new file mode 100755 index 000000000..6832a1d4c --- /dev/null +++ b/examples/invenio/solr/conf/xslt/example.xsl @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + <xsl:value-of select="$title"/> + + + +

+
+ This has been formatted by the sample "example.xsl" transform - + use your own XSLT to get a nicer page +
+ + + +
+ + + +
+ + + + +
+
+
+ + + + + + + + + + + + + + javascript:toggle("");? +
+ + exp + + + + + +
+ + +
+ + + + + + + +
    + +
  • +
    +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/examples/invenio/solr/conf/xslt/example_atom.xsl b/examples/invenio/solr/conf/xslt/example_atom.xsl new file mode 100755 index 000000000..e1c7d5a2a --- /dev/null +++ b/examples/invenio/solr/conf/xslt/example_atom.xsl @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + Example Solr Atom 1.0 Feed + + This has been formatted by the sample "example_atom.xsl" transform - + use your own XSLT to get a nicer Atom feed. + + + Apache Solr + solr-user@lucene.apache.org + + + + + + tag:localhost,2007:example + + + + + + + + + <xsl:value-of select="str[@name='name']"/> + + tag:localhost,2007: + + + + + + diff --git a/examples/invenio/solr/conf/xslt/example_rss.xsl b/examples/invenio/solr/conf/xslt/example_rss.xsl new file mode 100755 index 000000000..3e09e654d --- /dev/null +++ b/examples/invenio/solr/conf/xslt/example_rss.xsl @@ -0,0 +1,66 @@ + + + + + + + + + + + + + Example Solr RSS 2.0 Feed + http://localhost:8983/solr + + This has been formatted by the sample "example_rss.xsl" transform - + use your own XSLT to get a nicer RSS feed. + + en-us + http://localhost:8983/solr + + + + + + + + + + + <xsl:value-of select="str[@name='name']"/> + + http://localhost:8983/solr/select?q=id: + + + + + + + http://localhost:8983/solr/select?q=id: + + + + diff --git a/examples/invenio/solr/conf/xslt/luke.xsl b/examples/invenio/solr/conf/xslt/luke.xsl new file mode 100755 index 000000000..6e9a064d7 --- /dev/null +++ b/examples/invenio/solr/conf/xslt/luke.xsl @@ -0,0 +1,337 @@ + + + + + + + + + Solr Luke Request Handler Response + + + + + + + + + <xsl:value-of select="$title"/> + + + + + +

+ +

+
+ +
+ +

Index Statistics

+ +
+ +

Field Statistics

+ + + +

Document statistics

+ + + + +
+ + + + + +
+ +
+ + +
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+

+ +

+ +
+ +
+
+
+ + +
+ + 50 + 800 + 160 + blue + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ background-color: ; width: px; height: px; +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
  • + +
  • +
    +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + + + + + + + + + + + + + + + + +
diff --git a/examples/invenio/solr/conf/xslt/twitter.xsl b/examples/invenio/solr/conf/xslt/twitter.xsl new file mode 100755 index 000000000..0bc3e4087 --- /dev/null +++ b/examples/invenio/solr/conf/xslt/twitter.xsl @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + <xsl:value-of select="$title"/> + + + +

+
+ Demo Twitter indexing with Python +
+
+ + + + + + + +
+ + + +
+ + + +
+ + + + +
+
+
+ + + + + + + + + + + + + + javascript:toggle("");? +
+ + exp + + + + + +
+ + +
+ + + + + + + +
    + +
  • +
    +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/examples/twitter/etc/jetty.xml b/examples/twitter/etc/jetty.xml new file mode 100755 index 000000000..ebf3eebde --- /dev/null +++ b/examples/twitter/etc/jetty.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + org.mortbay.jetty.Request.maxFormContentSize + 1000000 + + + + + + + + + 10 + 50 + 60 + + + + + + + + + + + + + + + + + + + + + + 50000 + 1500 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /contexts + 1 + + + + + + + + + + + + + + + + + + + + + + /webapps + false + true + false + /etc/webdefault.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + /yyyy_mm_dd.request.log + 90 + true + false + GMT + + + + + + + + true + + true + + + diff --git a/examples/twitter/etc/logging.properties b/examples/twitter/etc/logging.properties new file mode 100644 index 000000000..6a54f49d7 --- /dev/null +++ b/examples/twitter/etc/logging.properties @@ -0,0 +1,12 @@ +# Default global logging level: +.level= SEVERE + +# Write to a file: +handlers= java.util.logging.FileHandler + +# Write log messages in XML format: +#java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter +java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter + +# Log to the current working directory, with log files named solrxxx.log +java.util.logging.FileHandler.pattern = /tmp/solr-test-%u.log \ No newline at end of file diff --git a/examples/twitter/etc/webdefault.xml b/examples/twitter/etc/webdefault.xml new file mode 100644 index 000000000..66bbdd5f8 --- /dev/null +++ b/examples/twitter/etc/webdefault.xml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + Default web.xml file. + This file is applied to a Web application before it's own WEB_INF/web.xml file + + + + + + + + + + org.mortbay.jetty.webapp.NoTLDJarPattern + start.jar|ant-.*\.jar|dojo-.*\.jar|jetty-.*\.jar|jsp-api-.*\.jar|junit-.*\.jar|servlet-api-.*\.jar|dnsns\.jar|rt\.jar|jsse\.jar|tools\.jar|sunpkcs11\.jar|sunjce_provider\.jar|xerces.*\.jar| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + org.mortbay.jetty.servlet.DefaultServlet + + acceptRanges + true + + + dirAllowed + true + + + redirectWelcome + false + + + maxCacheSize + 2000000 + + + maxCachedFileSize + 254000 + + + maxCachedFiles + 1000 + + + gzip + false + + + useFileMappedBuffer + false + + + 0 + + + default / + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + logVerbosityLevel + WARNING + + + fork + false + + + xpoweredBy + false + + + 0 + + + + jsp + *.jsp + *.jspf + *.jspx + *.xsp + *.JSP + *.JSPF + *.JSPX + *.XSP + + + + + + + + + + + + + + + + + + + + + + + + invoker + org.mortbay.jetty.servlet.Invoker + + verbose + true + + + nonContextServlets + true + + + dynamicParam + anyValue + + 1 + + + invoker /servlet/* + + + + + + + 30 + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + + + arISO-8859-6 + beISO-8859-5 + bgISO-8859-5 + caISO-8859-1 + csISO-8859-2 + daISO-8859-1 + deISO-8859-1 + elISO-8859-7 + enISO-8859-1 + esISO-8859-1 + etISO-8859-1 + fiISO-8859-1 + frISO-8859-1 + hrISO-8859-2 + huISO-8859-2 + isISO-8859-1 + itISO-8859-1 + iwISO-8859-8 + jaShift_JIS + koEUC-KR + ltISO-8859-2 + lvISO-8859-2 + mkISO-8859-5 + nlISO-8859-1 + noISO-8859-1 + plISO-8859-2 + ptISO-8859-1 + roISO-8859-2 + ruISO-8859-5 + shISO-8859-5 + skISO-8859-2 + slISO-8859-2 + sqISO-8859-2 + srISO-8859-5 + svISO-8859-1 + trISO-8859-9 + ukISO-8859-5 + zhGB2312 + zh_TWBig5 + + + + + + diff --git a/examples/twitter/solr/conf/admin-extra.html b/examples/twitter/solr/conf/admin-extra.html new file mode 100755 index 000000000..aa739da86 --- /dev/null +++ b/examples/twitter/solr/conf/admin-extra.html @@ -0,0 +1,31 @@ + + + diff --git a/examples/twitter/solr/conf/data-config-test-java.xml b/examples/twitter/solr/conf/data-config-test-java.xml new file mode 100644 index 000000000..3dec8773e --- /dev/null +++ b/examples/twitter/solr/conf/data-config-test-java.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/twitter/solr/conf/data-config.xml b/examples/twitter/solr/conf/data-config.xml new file mode 100644 index 000000000..3a0080d5a --- /dev/null +++ b/examples/twitter/solr/conf/data-config.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/twitter/solr/conf/elevate.xml b/examples/twitter/solr/conf/elevate.xml new file mode 100755 index 000000000..9b4caec69 --- /dev/null +++ b/examples/twitter/solr/conf/elevate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/examples/twitter/solr/conf/mapping-ISOLatin1Accent.txt b/examples/twitter/solr/conf/mapping-ISOLatin1Accent.txt new file mode 100755 index 000000000..ede774258 --- /dev/null +++ b/examples/twitter/solr/conf/mapping-ISOLatin1Accent.txt @@ -0,0 +1,246 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Syntax: +# "source" => "target" +# "source".length() > 0 (source cannot be empty.) +# "target".length() >= 0 (target can be empty.) + +# example: +# "À" => "A" +# "\u00C0" => "A" +# "\u00C0" => "\u0041" +# "ß" => "ss" +# "\t" => " " +# "\n" => "" + +# À => A +"\u00C0" => "A" + +# Á => A +"\u00C1" => "A" + +#  => A +"\u00C2" => "A" + +# à => A +"\u00C3" => "A" + +# Ä => A +"\u00C4" => "A" + +# Å => A +"\u00C5" => "A" + +# Æ => AE +"\u00C6" => "AE" + +# Ç => C +"\u00C7" => "C" + +# È => E +"\u00C8" => "E" + +# É => E +"\u00C9" => "E" + +# Ê => E +"\u00CA" => "E" + +# Ë => E +"\u00CB" => "E" + +# Ì => I +"\u00CC" => "I" + +# Í => I +"\u00CD" => "I" + +# Î => I +"\u00CE" => "I" + +# Ï => I +"\u00CF" => "I" + +# IJ => IJ +"\u0132" => "IJ" + +# Ð => D +"\u00D0" => "D" + +# Ñ => N +"\u00D1" => "N" + +# Ò => O +"\u00D2" => "O" + +# Ó => O +"\u00D3" => "O" + +# Ô => O +"\u00D4" => "O" + +# Õ => O +"\u00D5" => "O" + +# Ö => O +"\u00D6" => "O" + +# Ø => O +"\u00D8" => "O" + +# Œ => OE +"\u0152" => "OE" + +# Þ +"\u00DE" => "TH" + +# Ù => U +"\u00D9" => "U" + +# Ú => U +"\u00DA" => "U" + +# Û => U +"\u00DB" => "U" + +# Ü => U +"\u00DC" => "U" + +# Ý => Y +"\u00DD" => "Y" + +# Ÿ => Y +"\u0178" => "Y" + +# à => a +"\u00E0" => "a" + +# á => a +"\u00E1" => "a" + +# â => a +"\u00E2" => "a" + +# ã => a +"\u00E3" => "a" + +# ä => a +"\u00E4" => "a" + +# å => a +"\u00E5" => "a" + +# æ => ae +"\u00E6" => "ae" + +# ç => c +"\u00E7" => "c" + +# è => e +"\u00E8" => "e" + +# é => e +"\u00E9" => "e" + +# ê => e +"\u00EA" => "e" + +# ë => e +"\u00EB" => "e" + +# ì => i +"\u00EC" => "i" + +# í => i +"\u00ED" => "i" + +# î => i +"\u00EE" => "i" + +# ï => i +"\u00EF" => "i" + +# ij => ij +"\u0133" => "ij" + +# ð => d +"\u00F0" => "d" + +# ñ => n +"\u00F1" => "n" + +# ò => o +"\u00F2" => "o" + +# ó => o +"\u00F3" => "o" + +# ô => o +"\u00F4" => "o" + +# õ => o +"\u00F5" => "o" + +# ö => o +"\u00F6" => "o" + +# ø => o +"\u00F8" => "o" + +# œ => oe +"\u0153" => "oe" + +# ß => ss +"\u00DF" => "ss" + +# þ => th +"\u00FE" => "th" + +# ù => u +"\u00F9" => "u" + +# ú => u +"\u00FA" => "u" + +# û => u +"\u00FB" => "u" + +# ü => u +"\u00FC" => "u" + +# ý => y +"\u00FD" => "y" + +# ÿ => y +"\u00FF" => "y" + +# ff => ff +"\uFB00" => "ff" + +# fi => fi +"\uFB01" => "fi" + +# fl => fl +"\uFB02" => "fl" + +# ffi => ffi +"\uFB03" => "ffi" + +# ffl => ffl +"\uFB04" => "ffl" + +# ſt => ft +"\uFB05" => "ft" + +# st => st +"\uFB06" => "st" diff --git a/examples/twitter/solr/conf/protwords.txt b/examples/twitter/solr/conf/protwords.txt new file mode 100755 index 000000000..1dfc0abec --- /dev/null +++ b/examples/twitter/solr/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/examples/twitter/solr/conf/schema.xml b/examples/twitter/solr/conf/schema.xml new file mode 100755 index 000000000..8c5ad68dc --- /dev/null +++ b/examples/twitter/solr/conf/schema.xml @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/twitter/solr/conf/scripts.conf b/examples/twitter/solr/conf/scripts.conf new file mode 100755 index 000000000..f58b262ae --- /dev/null +++ b/examples/twitter/solr/conf/scripts.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +user= +solr_hostname=localhost +solr_port=8983 +rsyncd_port=18983 +data_dir= +webapp_name=solr +master_host= +master_data_dir= +master_status_dir= diff --git a/examples/twitter/solr/conf/solrconfig.xml b/examples/twitter/solr/conf/solrconfig.xml new file mode 100755 index 000000000..023e94490 --- /dev/null +++ b/examples/twitter/solr/conf/solrconfig.xml @@ -0,0 +1,1106 @@ + + + + + + ${solr.abortOnConfigurationError:true} + + + + + + + + + + + + + + + + ${solr.data.dir:./solr/data} + + + + + + false + + 10 + + + + + 32 + + 10000 + 1000 + 10000 + + + + + + + + + + + + + native + + + + + + + false + 32 + 10 + + + + + + + + false + + + true + + + + + + + + 1 + + 0 + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + true + + + + + + + + 20 + + + 200 + + + + + + + + + + + + + solr rocks010 + static firstSearcher warming query from solrconfig.xml + {!iq iq.mode=maxinv}refersto:recid:100010invenio + {!iq iq.mode=maxinv}citedby:recid:100010invenio + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + explicit + + + + + + + + + + + query + invenio-formatter + facet + mlt + highlight + stats + debug + + + + iq + explicit + invenio + maxinv + fulltext + AND + + + + + + + + + + + + + dismax + explicit + 0.01 + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + + + text^0.2 features^1.1 name^1.5 manu^1.4 manu_exact^1.9 + + + popularity^0.5 recip(price,1,1000,1000)^0.3 + + + id,name,price,score + + + 2<-1 5<-2 6<90% + + 100 + *:* + + text features name + + 0 + + name + regex + + + + + + + dismax + explicit + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 + 2<-1 5<-2 6<90% + + incubationdate_dt:[* TO NOW/DAY-1MONTH]^2.2 + + + + inStock:true + + + + cat + manu_exact + price:[* TO 500] + price:[500 TO *] + + + + + + + + + + textSpell + + + default + name + ./spellchecker + + + + + + + + + + + + + + + + false + + false + + 1 + + + spellcheck + + + + + + + + true + + + tvComponent + + + + + + + + + default + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + 20 + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + true + default + true + + name + id + + features + + true + + + + false + + + clusteringComponent + + + + + + + + text + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + true + + + termsComponent + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + standard + solrpingquery + all + + + + + + + explicit + true + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + solr + + + + + + + + + data-config.xml + false + false + + + + + data-config.xml + false + false + + + + + data-config-test-java.xml + false + false + + + + + + diff --git a/examples/twitter/solr/conf/spellings.txt b/examples/twitter/solr/conf/spellings.txt new file mode 100755 index 000000000..d7ede6f56 --- /dev/null +++ b/examples/twitter/solr/conf/spellings.txt @@ -0,0 +1,2 @@ +pizza +history \ No newline at end of file diff --git a/examples/twitter/solr/conf/stopwords.txt b/examples/twitter/solr/conf/stopwords.txt new file mode 100755 index 000000000..b5824da32 --- /dev/null +++ b/examples/twitter/solr/conf/stopwords.txt @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +#Standard english stop words taken from Lucene's StopAnalyzer +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with + diff --git a/examples/twitter/solr/conf/synonyms.txt b/examples/twitter/solr/conf/synonyms.txt new file mode 100755 index 000000000..b0e31cb7e --- /dev/null +++ b/examples/twitter/solr/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + diff --git a/examples/twitter/solr/conf/xslt/example.xsl b/examples/twitter/solr/conf/xslt/example.xsl new file mode 100755 index 000000000..6832a1d4c --- /dev/null +++ b/examples/twitter/solr/conf/xslt/example.xsl @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + <xsl:value-of select="$title"/> + + + +

+
+ This has been formatted by the sample "example.xsl" transform - + use your own XSLT to get a nicer page +
+ + + +
+ + + +
+ + + + +
+
+
+ + + + + + + + + + + + + + javascript:toggle("");? +
+ + exp + + + + + +
+ + +
+ + + + + + + +
    + +
  • +
    +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/examples/twitter/solr/conf/xslt/example_atom.xsl b/examples/twitter/solr/conf/xslt/example_atom.xsl new file mode 100755 index 000000000..e1c7d5a2a --- /dev/null +++ b/examples/twitter/solr/conf/xslt/example_atom.xsl @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + Example Solr Atom 1.0 Feed + + This has been formatted by the sample "example_atom.xsl" transform - + use your own XSLT to get a nicer Atom feed. + + + Apache Solr + solr-user@lucene.apache.org + + + + + + tag:localhost,2007:example + + + + + + + + + <xsl:value-of select="str[@name='name']"/> + + tag:localhost,2007: + + + + + + diff --git a/examples/twitter/solr/conf/xslt/example_rss.xsl b/examples/twitter/solr/conf/xslt/example_rss.xsl new file mode 100755 index 000000000..3e09e654d --- /dev/null +++ b/examples/twitter/solr/conf/xslt/example_rss.xsl @@ -0,0 +1,66 @@ + + + + + + + + + + + + + Example Solr RSS 2.0 Feed + http://localhost:8983/solr + + This has been formatted by the sample "example_rss.xsl" transform - + use your own XSLT to get a nicer RSS feed. + + en-us + http://localhost:8983/solr + + + + + + + + + + + <xsl:value-of select="str[@name='name']"/> + + http://localhost:8983/solr/select?q=id: + + + + + + + http://localhost:8983/solr/select?q=id: + + + + diff --git a/examples/twitter/solr/conf/xslt/luke.xsl b/examples/twitter/solr/conf/xslt/luke.xsl new file mode 100755 index 000000000..6e9a064d7 --- /dev/null +++ b/examples/twitter/solr/conf/xslt/luke.xsl @@ -0,0 +1,337 @@ + + + + + + + + + Solr Luke Request Handler Response + + + + + + + + + <xsl:value-of select="$title"/> + + + + + +

+ +

+
+ +

Index Statistics

+ +
+ +

Field Statistics

+ + + +

Document statistics

+ + + + + + + + + + +
+ +
+ + +
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+

+ +

+ +
+ +
+
+
+ + +
+ + 50 + 800 + 160 + blue + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ background-color: ; width: px; height: px; +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
  • + +
  • +
    +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + + + + + + + + + + + + + + + + + diff --git a/examples/twitter/solr/conf/xslt/twitter.xsl b/examples/twitter/solr/conf/xslt/twitter.xsl new file mode 100755 index 000000000..0bc3e4087 --- /dev/null +++ b/examples/twitter/solr/conf/xslt/twitter.xsl @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + <xsl:value-of select="$title"/> + + + +

+
+ Demo Twitter indexing with Python +
+
+ + + + + + + +
+ + + +
+ + + +
+ + + + +
+
+
+ + + + + + + + + + + + + + javascript:toggle("");? +
+ + exp + + + + + +
+ + +
+ + + + + + + +
    + +
  • +
    +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/lib/LICENSE.jzlib.txt b/lib/LICENSE.jzlib.txt new file mode 100644 index 000000000..cdce5007d --- /dev/null +++ b/lib/LICENSE.jzlib.txt @@ -0,0 +1,29 @@ +JZlib 0.0.* were released under the GNU LGPL license. Later, we have switched +over to a BSD-style license. + +------------------------------------------------------------------------------ +Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, +INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/junit-3.8.2.jar b/lib/junit-3.8.2.jar new file mode 100755 index 0000000000000000000000000000000000000000..c8f711d050eff209321f799d85ebb3bbe305d481 GIT binary patch literal 120640 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
4LLYqQ5{NoW8NO+g^8MA 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 A>i_@% literal 0 HcmV?d00001 diff --git a/lib/jzlib-1.0.7.jar b/lib/jzlib-1.0.7.jar new file mode 100644 index 0000000000000000000000000000000000000000..dd760655ea03944d1c16accacc8f45b0b578ca1f GIT binary patch literal 49636 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 literal 0 HcmV?d00001 diff --git a/src/java/invenio/montysolr/JettyRunner.java b/src/java/invenio/montysolr/JettyRunner.java new file mode 100644 index 000000000..b7d95bcd5 --- /dev/null +++ b/src/java/invenio/montysolr/JettyRunner.java @@ -0,0 +1,147 @@ +package invenio.montysolr; + +import java.io.File; +import java.net.URL; +import org.apache.commons.io.IOUtils; +import org.mortbay.jetty.Connector; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.bio.SocketConnector; +import org.mortbay.jetty.webapp.WebAppContext; +import org.mortbay.jetty.webapp.WebAppClassLoader; + + +public class JettyRunner { + int port = 8983; + String context = "/test"; + String webroot = "/x/dev/workspace/test-solr/webapp"; + Server server; + boolean isRunning = false; + + public JettyRunner() { + System.out.println("JettyRunner loaded"); + } + + public JettyRunner(String[] args) throws Exception { + System.out.println("JettyRunner loaded"); + this.configure(args); + } + + public void configure(String[] params) throws Exception { + for (int i = 0; i < params.length; i++) { + String t = params[i]; + if (t.contains("port")) { + port = new Integer(params[i + 1]); + } else if (t.contains("context")) { + context = params[i + 1]; + } else if (t.contains("solr.home")) { + System.setProperty("solr.solr.home", params[i + 1]); + } else if (t.contains("webroot")) { + webroot = params[i + 1]; + } else { + throw new Exception("Unknown option " + t); + } + i++; + } + + File h = new File(System.getProperty("solr.solr.home")); + if ( !h.exists()) { + throw new Exception("solr.solr.home not set or not exists"); + } + + } + + public void start() throws Exception { + if (!isRunning) { + + server = new Server(port); + + WebAppContext ctx = new WebAppContext(server, webroot, context); + + + // this sets the normal java class-loading policy, when system + // classes (and classes loaded first) have higher priority + // this is imporant for our singleton to work, otherwise there + // are different classloaders and the singletons are not singletons + // across webapps + ctx.setParentLoaderPriority(true); + + // also this works and has the same effect (I don't know what are + // implications of one or the other method + //ctx.setClassLoader(this.getClass().getClassLoader()); + + SocketConnector connector = new SocketConnector(); + connector.setMaxIdleTime(1000 * 60 * 60); + connector.setSoLingerTime(-1); + connector.setPort(port); + server.setConnectors(new Connector[] { connector }); + + server.setStopAtShutdown(true); + + server.start(); + port = connector.getLocalPort(); + isRunning = true; + } + } + + public void stop() throws Exception { + if (isRunning) { + server.stop(); + isRunning = false; + } + } + + private void testJSP() throws Exception + { + // Currently not an extensive test, but it does fire up the JSP pages and make + // sure they compile ok + + String queryPath = "http://localhost:"+port+context+"/"; + String adminPath = "http://localhost:"+port+context+"/admin/"; + + String html = IOUtils.toString( new URL(queryPath).openStream() ); + assert html.contains("") + 12; + System.out.println(html); + + // special caching query + html = IOUtils.toString( new URL(queryPath+"select/?q=*%3A*&version=2.2&start=0&rows=10&indent=on&qt=recidspython").openStream()); + start_pos = html.indexOf("name=\"docs\">") + 12; + System.out.println(html); + } + + /** + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception { + System.out.println("bootstrap.Main loader = " + JettyRunnerPythonVM.class.getClassLoader().toString()); + JettyRunnerPythonVM jr = null; + try { + jr = new JettyRunnerPythonVM(); + ClassLoader currentContextLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(jr.getClass().getClassLoader()); // load jetty + //Thread.currentThread().setContextClassLoader(currentContextLoader ); + + jr.configure(args); + + if (jr.daemonMode) { + jr.join(); + } + else { + jr.start(); + jr.testJSP(); + jr.stop(); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + jr.stop(); + } + + } + +} diff --git a/src/java/invenio/montysolr/SolrRunner.java b/src/java/invenio/montysolr/SolrRunner.java new file mode 100644 index 000000000..70332b5c3 --- /dev/null +++ b/src/java/invenio/montysolr/SolrRunner.java @@ -0,0 +1,30 @@ +package invenio.montysolr; + +import org.apache.solr.client.solrj.embedded.JettySolrRunner; +import java.io.File; +import java.lang.System; + + +public class SolrRunner { + + static File rootDir = new File("/x/dev/workspace/apache-solr-1.4.1/example/"); + static File homeDir = new File(rootDir, "solr"); + static File dataDir = new File(homeDir, "data"); + static File confDir = new File(homeDir, "conf"); + + + public static JettySolrRunner createJetty() throws Exception { + System.setProperty("solr.solr.home", homeDir.toString()); + System.setProperty("solr.data.dir", dataDir.toString()); + JettySolrRunner jetty = new JettySolrRunner("/solr", 0, rootDir.toString() + "/etc/jetty.xml"); + jetty.start(); + return jetty; + } + + public static void main(String[] args) throws Exception { + JettySolrRunner jetty = createJetty(); + System.out.println(jetty); + System.out.println(jetty.getLocalPort()); + } + +} diff --git a/src/java/invenio/montysolr/examples/TwitterAPIHandler.java b/src/java/invenio/montysolr/examples/TwitterAPIHandler.java new file mode 100644 index 000000000..a6d7758c3 --- /dev/null +++ b/src/java/invenio/montysolr/examples/TwitterAPIHandler.java @@ -0,0 +1,54 @@ +package invenio.montysolr.examples; + +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; + + +public class TwitterAPIHandler extends RequestHandlerBase{ + @Override + + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception + { + + long start = System.currentTimeMillis(); + + PythonMessage message = MontySolrVM.INSTANCE + .createMessage("twitter_api") + .setSender(this.getClass().getSimpleName()) + .setSolrQueryRequest(req) + .setSolrQueryResponse(rsp); + + MontySolrVM.INSTANCE.sendMessage(message); + + long end = System.currentTimeMillis(); + + rsp.add( "QTime", end-start); + } + + + //////////////////////// SolrInfoMBeans methods ////////////////////// + + @Override + public String getVersion() { + return ""; + } + + @Override + public String getDescription() { + return "Adds new Tweets each time search term is passed"; + } + + @Override + public String getSourceId() { + return ""; + } + + @Override + public String getSource() { + return ""; + } +} diff --git a/src/java/invenio/montysolr/jni/BasicBridge.java b/src/java/invenio/montysolr/jni/BasicBridge.java new file mode 100644 index 000000000..77d63ba58 --- /dev/null +++ b/src/java/invenio/montysolr/jni/BasicBridge.java @@ -0,0 +1,60 @@ +package invenio.montysolr.jni; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.solr.core.SolrResourceLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is abstract class that holds testing methods and basic methods + * of each bridge + * @author rca + * + */ + +public abstract class BasicBridge { + + public static final Logger log = LoggerFactory.getLogger(BasicBridge.class); + + protected String bridgeName = null; + + public String getName() { + return this.bridgeName; + } + public void setName(String name) { + this.bridgeName = name; + } + + // ------------- java testing methods ----------------- + + + public void testPrint() { + System.out.println("java is printing, instance: " + this.toString() + + " from thread id: " + Thread.currentThread().getId()); + } + + public String testReturnString() { + return "java is printing, instance: " + this.toString() + + " from thread id: " + Thread.currentThread().getId(); + } + + public List testReturnListOfStrings() { + ArrayList l = new ArrayList(); + l.add(getName()); + l.add(this.toString()); + l.add(Long.toString(Thread.currentThread().getId())); + return l; + } + + public List testReturnListOfIntegers() { + ArrayList l = new ArrayList(); + l.add(0); + l.add(1); + return l; + } + + + +} diff --git a/src/java/invenio/montysolr/jni/MontySolrBridge.java b/src/java/invenio/montysolr/jni/MontySolrBridge.java new file mode 100644 index 000000000..e2d30585b --- /dev/null +++ b/src/java/invenio/montysolr/jni/MontySolrBridge.java @@ -0,0 +1,63 @@ +package invenio.montysolr.jni; + + + +import org.apache.jcc.PythonVM; + + + +/** + * This class is used for calling Invenio from inside JavaVM + * + * @author rca + * + */ + +public class MontySolrBridge extends BasicBridge implements PythonBridge { + + private long pythonObject; + protected String bridgeName; + + + public void pythonExtension(long pythonObject) { + this.pythonObject = pythonObject; + } + + public long pythonExtension() { + return this.pythonObject; + } + + public void finalize() throws Throwable { + pythonDecRef(); + } + + public native void pythonDecRef(); + + + + /** + * The main method that passes the PythonMessage instance + * to the remote site over the JNI/JCC bridge + * @param message + */ + @Override + public void sendMessage(PythonMessage message) { + PythonVM vm = PythonVM.get(); + vm.acquireThreadState(); + receive_message(message); + vm.releaseThreadState(); + } + public native void receive_message(PythonMessage message); + + + /** + * Just some testing methods, should be removed after + * the code stabilizes + */ + public native void test_print(); + public native String test_return_string(); + +} + + + diff --git a/src/java/invenio/montysolr/jni/MontySolrVM.java b/src/java/invenio/montysolr/jni/MontySolrVM.java new file mode 100644 index 000000000..5edae2b99 --- /dev/null +++ b/src/java/invenio/montysolr/jni/MontySolrVM.java @@ -0,0 +1,117 @@ +package invenio.montysolr.jni; + +import org.apache.jcc.PythonException; +import org.apache.jcc.PythonVM; + + + +import java.util.concurrent.Semaphore; + +public enum MontySolrVM { + INSTANCE; + + private PythonVM vm = null; + + private Semaphore semaphore = + new Semaphore((System.getProperty("montysolr.max_workers") != null ? new Integer(System.getProperty("montysolr.max_workers")) : 1), true); + + public PythonVM start(String programName) { + if (vm == null) + vm = PythonVM.start(programName); + return vm; + } + + /** + * Creates a new instance of the bridge over the Python waters. + * This instance can be used to send the PythonMessage, but until + * sendMessage is called, it does nothing + * @return {@link PythonBridge} + */ + public PythonBridge getBridge() { + return PythonVMBridge.start(); + } + + /** + * Creates a PythonMessage that wraps all the parameters that will be delivered + * to the remote side. It will also contain any return value + * @param receiver + * @return void + */ + public PythonMessage createMessage(String receiver) { + return new PythonMessage(receiver); + } + + /** + * Passes the message over to the remote site, this method is just a factory + * the passing is done by the Bridge itself + * @throws InterruptedException + */ + + public void sendMessage(PythonMessage message) throws InterruptedException { + PythonBridge b = getBridge(); + try { + semaphore.acquire(); + b.sendMessage(message); + } finally { + semaphore.release(); + } + + } + + + +} + +/** + * This class MUST NOT be a singleton. It serves the purpose of communicating w + * Python VM. You get the bridge and use methods of the bridge. + * + * @author rca + * + */ + +class PythonVMBridge { + static protected PythonBridge bridge; + + protected PythonVMBridge() { + + } + + static public PythonBridge start() throws PythonException { + if (System.getProperty("python.bridge") == null) { + return start("SimpleBridge"); + } + else { + return start(System.getProperty("python.bridge")); + } + } + + static public PythonBridge start(String bridgeName) throws PythonException { + if (bridgeName == "montysolr.java_bridge.SimpleBridge" || + bridgeName == "SimpleBridge") { + return start("montysolr.java_bridge", "SimpleBridge"); + } + return bridge; + } + + static public PythonBridge start(String moduleName, String className) throws PythonException + { + if (bridge == null) + { + PythonVM vm = PythonVM.get(); + bridge = (PythonBridge)vm.instantiate(moduleName, className); + bridge.setName(moduleName+'.'+className); + } + + return bridge; + } + + static public PythonBridge get() throws PythonException { + return start("SimpleBridge"); + //return bridge; + } + + +} + + diff --git a/src/java/invenio/montysolr/jni/PythonBridge.java b/src/java/invenio/montysolr/jni/PythonBridge.java new file mode 100644 index 000000000..9efcd2ed9 --- /dev/null +++ b/src/java/invenio/montysolr/jni/PythonBridge.java @@ -0,0 +1,36 @@ +/** + * + */ +package invenio.montysolr.jni; + + +/** + * All the bridge implementation between Java<->Python are required to have + * these basic methods + * + * @author rca + * + */ +public interface PythonBridge { + + /** + * Returns the name of this Bridge implementation + * @return + */ + public String getName(); + + /** + * Sets the name of the bridge after the Bridge was instantiated by the + * PythonVMBridge + * @void + */ + public void setName(String name); + + + /** + * Generic method for sending a PythonMessage into the remote JNI side + * @param message + */ + public void sendMessage(PythonMessage message); + +} diff --git a/src/java/invenio/montysolr/jni/PythonMessage.java b/src/java/invenio/montysolr/jni/PythonMessage.java new file mode 100644 index 000000000..cb816c566 --- /dev/null +++ b/src/java/invenio/montysolr/jni/PythonMessage.java @@ -0,0 +1,122 @@ +package invenio.montysolr.jni; + +import java.util.AbstractCollection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrResponse; +import org.apache.solr.core.SolrResourceLoader; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PythonMessage extends HashMap{ + + /** + * + */ + private static final long serialVersionUID = -3744935985066647405L; + public static final Logger log = LoggerFactory.getLogger(PythonMessage.class); + + + public PythonMessage(String receiver) { + this.put("receiver", receiver); + } + + public String getSender() { + return (String) this.get("sender"); + } + + public PythonMessage setSender(String sender) { + this.put("sender", sender); + return this; + } + + public String getReceiver() { + return (String) this.get("receiver"); + } + + public PythonMessage setReceiver(String receiver) { + this.put("receiver", receiver); + return this; + } + + + public SolrQueryRequest getSolrQueryRequest() { + return (SolrQueryRequest) this.get("SolrQueryRequest"); + } + public PythonMessage setSolrQueryRequest(SolrQueryRequest sqr) { + this.put("SolrQueryRequest", sqr); + return this; + } + + public SolrQueryResponse getSolrQueryResponse() { + return (SolrQueryResponse) this.get("SolrQueryResponse"); + } + public PythonMessage setSolrQueryResponse(SolrQueryResponse srp) { + this.put("SolrQueryResponse", srp); + return this; + } + + public PythonMessage setParam(String name, Object value) { + this.put(name, value); + return this; + } + + public Object getParam(String name) { + return this.get(name); + } + + public int[] getParamArray_int(String name) { + return (int[]) this.get(name); + } + public String[] getParamArray_str(String name) { + return (String[]) this.get(name); + } + + public Object getResults() { + return this.get("#result"); + } + + public void setResults(Object result) { + this.put("#result", result); + } + + + public String toString() { + Set> s = this.entrySet(); + StringBuilder out = new StringBuilder(); + for (Entry e: s) { + out.append(e.getKey()); + out.append("="); + Object v = e.getValue(); + if (v instanceof AbstractCollection) { + if (((AbstractCollection) v).size() > 10) { + out.append("@" + v.getClass()); + } + else { + out.append(v); + } + } + else { + out.append(v); + } + out.append(","); + } + return out.toString(); + } + + public void threadInfo(String s) { + log.info("[Python] " + s + this.getInfo()); + } + + private String getInfo() { + return " [Thread=" + Thread.currentThread().getName() + " id=" + Thread.currentThread().getId() + + " time=" + System.currentTimeMillis() + "]"; + } + +} diff --git a/src/java/invenio/montysolr/util/DebuggingMethods.java b/src/java/invenio/montysolr/util/DebuggingMethods.java new file mode 100644 index 000000000..c72610e02 --- /dev/null +++ b/src/java/invenio/montysolr/util/DebuggingMethods.java @@ -0,0 +1,25 @@ +package invenio.montysolr.util; + +import org.apache.jcc.PythonVM; + +public class DebuggingMethods { + + public static void discoverImportableModules() { + String[] modules = {"montysolr_java", "montysolr_java.solrpye.invenio", "montysolr_java.solrpye", "solrpye.invenio"}; + String[] classes = {"TestX", "InvenioSolrBridge", "Emql"}; + Object m = null; + PythonVM vm = PythonVM.get(); + for (int i=0;i= size) + return false; + return (bytes[bit / 8] & BIT_MASK[bit % 8]) != 0; + } + + protected static void setBit(int bit, byte[] bytes) { + int size = bytes == null ? 0 : bytes.length * 8; + if (bit >= size) + throw new ArrayIndexOutOfBoundsException("Byte array too small"); + bytes[bit / 8] |= BIT_MASK[bit % 8]; + } + + public static void main(String[] args) throws DataFormatException, IOException { + + int min = 0; + int max = 5000; + InvenioBitSet ibs = new InvenioBitSet(max); + for (int i = 0; i < 500; i++) { + int r = min + (int) (Math.random() * ((max - min) + 1)); + ibs.set(r); + } + ByteArrayOutputStream b = ibs.fastDump(); + InvenioBitSet bs; + bs = InvenioBitSet.fastLoad(b.toByteArray()); + System.out.println("set lengths: " + ibs.cardinality() + " - " + bs.cardinality()); + System.out.println("Set equals? " + ibs.equals(bs)); + + } +} diff --git a/src/java/org/ads/solr/InvenioBitSet.java b/src/java/org/ads/solr/InvenioBitSet.java new file mode 100755 index 000000000..1fb205e6f --- /dev/null +++ b/src/java/org/ads/solr/InvenioBitSet.java @@ -0,0 +1,73 @@ +package org.ads.solr; + +import java.io.*; +import java.util.BitSet; +import java.util.Arrays; + +public class InvenioBitSet extends BitSet { + + private static final long serialVersionUID = 1L; + + public InvenioBitSet() { + super(); + } + + public InvenioBitSet(int nbits) { + super(nbits); + } + + // TODO: remove the trailing 8 bytes (added by intbitset format) and test + public InvenioBitSet(byte[] bytes) { + this(bytes == null? 0 : bytes.length * 8); + for (int i = 0; i < size(); i++) { + if (isBitOn(i, bytes)) + set(i); + } + } + + // convert to a byte array to be used as intbitset in Invenio + public byte[] toByteArray() { + + if (size() == 0) + return new byte[0]; + + // Find highest bit + int hiBit = -1; + for (int i = 0; i < size(); i++) { + if (get(i)) + hiBit = i; + } + + // was: int n = (hiBit + 8) / 8; + // +128 (64 for trailing zeros used in intbitset and 64 to avoid trancating) + int n = ((hiBit + 128) / 64) * 8; + byte[] bytes = new byte[n]; + if (n == 0) + return bytes; + + Arrays.fill(bytes, (byte)0); + for (int i=0; i= size) + return false; + return (bytes[bit/8] & BIT_MASK[bit%8]) != 0; + } + + protected static void setBit(int bit, byte[] bytes) { + int size = bytes == null ? 0 : bytes.length*8; + if (bit >= size) + throw new ArrayIndexOutOfBoundsException("Byte array too small"); + bytes[bit/8] |= BIT_MASK[bit%8]; + } +} diff --git a/src/java/org/apache/lucene/queryParser/CharStream.java b/src/java/org/apache/lucene/queryParser/CharStream.java new file mode 100644 index 000000000..fc9606f0d --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/CharStream.java @@ -0,0 +1,115 @@ +/* Generated By:JavaCC: Do not edit this line. CharStream.java Version 5.0 */ +/* JavaCCOptions:STATIC=false,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +package org.apache.lucene.queryParser; + +/** + * This interface describes a character stream that maintains line and + * column number positions of the characters. It also has the capability + * to backup the stream to some extent. An implementation of this + * interface is used in the TokenManager implementation generated by + * JavaCCParser. + * + * All the methods except backup can be implemented in any fashion. backup + * needs to be implemented correctly for the correct operation of the lexer. + * Rest of the methods are all used to get information like line number, + * column number and the String that constitutes a token and are not used + * by the lexer. Hence their implementation won't affect the generated lexer's + * operation. + */ + +public +interface CharStream { + + /** + * Returns the next character from the selected input. The method + * of selecting the input is the responsibility of the class + * implementing this interface. Can throw any java.io.IOException. + */ + char readChar() throws java.io.IOException; + + @Deprecated + /** + * Returns the column position of the character last read. + * @deprecated + * @see #getEndColumn + */ + int getColumn(); + + @Deprecated + /** + * Returns the line number of the character last read. + * @deprecated + * @see #getEndLine + */ + int getLine(); + + /** + * Returns the column number of the last character for current token (being + * matched after the last call to BeginTOken). + */ + int getEndColumn(); + + /** + * Returns the line number of the last character for current token (being + * matched after the last call to BeginTOken). + */ + int getEndLine(); + + /** + * Returns the column number of the first character for current token (being + * matched after the last call to BeginTOken). + */ + int getBeginColumn(); + + /** + * Returns the line number of the first character for current token (being + * matched after the last call to BeginTOken). + */ + int getBeginLine(); + + /** + * Backs up the input stream by amount steps. Lexer calls this method if it + * had already read some characters, but could not use them to match a + * (longer) token. So, they will be used again as the prefix of the next + * token and it is the implemetation's responsibility to do this right. + */ + void backup(int amount); + + /** + * Returns the next character that marks the beginning of the next token. + * All characters must remain in the buffer between two successive calls + * to this method to implement backup correctly. + */ + char BeginToken() throws java.io.IOException; + + /** + * Returns a string made up of characters from the marked token beginning + * to the current buffer position. Implementations have the choice of returning + * anything that they want to. For example, for efficiency, one might decide + * to just return null, which is a valid implementation. + */ + String GetImage(); + + /** + * Returns an array of characters that make up the suffix of length 'len' for + * the currently matched token. This is used to build up the matched string + * for use in actions in the case of MORE. A simple and inefficient + * implementation of this is as follows : + * + * { + * String t = GetImage(); + * return t.substring(t.length() - len, t.length()).toCharArray(); + * } + */ + char[] GetSuffix(int len); + + /** + * The lexer calls this function to indicate that it is done with the stream + * and hence implementations can free any resources held by this class. + * Again, the body of this function can be just empty and it will not + * affect the lexer's operation. + */ + void Done(); + +} +/* JavaCC - OriginalChecksum=6b854f7f279fcc2b052037ffc369be2d (do not edit this line) */ diff --git a/src/java/org/apache/lucene/queryParser/InvenioQueryParser.java b/src/java/org/apache/lucene/queryParser/InvenioQueryParser.java new file mode 100644 index 000000000..77cbc567c --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/InvenioQueryParser.java @@ -0,0 +1,1932 @@ +/* Generated By:JavaCC: Do not edit this line. InvenioQueryParser.java */ +package org.apache.lucene.queryParser; + +import java.io.IOException; +import java.io.StringReader; +import java.text.Collator; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Vector; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.CachingTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.DateField; +import org.apache.lucene.document.DateTools; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.Parameter; +import org.apache.lucene.util.Version; + +/** + * This class is generated by JavaCC. The most important method is + * {@link #parse(String)}. + * + * The syntax for query strings is as follows: + * A Query is a series of clauses. + * A clause may be prefixed by: + *
    + *
  • a plus (+) or a minus (-) sign, indicating + * that the clause is required or prohibited respectively; or + *
  • a term followed by a colon, indicating the field to be searched. + * This enables one to construct queries which search multiple fields. + *
+ * + * A clause may be either: + *
    + *
  • a term, indicating all the documents that contain this term; or + *
  • a nested query, enclosed in parentheses. Note that this may be used + * with a +/- prefix to require any of a set of + * terms. + *
+ * + * Thus, in BNF, the query grammar is: + *
+ *   Query  ::= ( Clause )*
+ *   Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")" )
+ * 
+ * + *

+ * Examples of appropriately formatted queries can be found in the query syntax + * documentation. + *

+ * + *

+ * In {@link TermRangeQuery}s, InvenioQueryParser tries to detect date values, e.g. + * date:[6/1/2005 TO 6/4/2005] produces a range query that searches + * for "date" fields between 2005-06-01 and 2005-06-04. Note that the format + * of the accepted input depends on {@link #setLocale(Locale) the locale}. + * By default a date is converted into a search term using the deprecated + * {@link DateField} for compatibility reasons. + * To use the new {@link DateTools} to convert dates, a + * {@link org.apache.lucene.document.DateTools.Resolution} has to be set. + *

+ *

+ * The date resolution that shall be used for RangeQueries can be set + * using {@link #setDateResolution(DateTools.Resolution)} + * or {@link #setDateResolution(String, DateTools.Resolution)}. The former + * sets the default date resolution for all fields, whereas the latter can + * be used to set field specific date resolutions. Field specific date + * resolutions take, if set, precedence over the default date resolution. + *

+ *

+ * If you use neither {@link DateField} nor {@link DateTools} in your + * index, you can create your own + * query parser that inherits InvenioQueryParser and overwrites + * {@link #getRangeQuery(String, String, String, boolean)} to + * use a different method for date conversion. + *

+ * + *

Note that InvenioQueryParser is not thread-safe.

+ * + *

NOTE: there is a new InvenioQueryParser in contrib, which matches + * the same syntax as this class, but is more modular, + * enabling substantial customization to how a query is created. + * + * + *

NOTE: You must specify the required {@link Version} + * compatibility when creating InvenioQueryParser: + *

+ */ +public class InvenioQueryParser implements InvenioQueryParserConstants { + + private static final int CONJ_NONE = 0; + private static final int CONJ_AND = 1; + private static final int CONJ_OR = 2; + + private static final int MOD_NONE = 0; + private static final int MOD_NOT = 10; + private static final int MOD_REQ = 11; + private static final int MOD_SECOND = 12; + + // make it possible to call setDefaultOperator() without accessing + // the nested class: + /** Alternative form of InvenioQueryParser.Operator.AND */ + public static final Operator AND_OPERATOR = Operator.AND; + /** Alternative form of InvenioQueryParser.Operator.OR */ + public static final Operator OR_OPERATOR = Operator.OR; + + /** The actual operator that parser uses to combine query terms */ + private Operator operator = OR_OPERATOR; + + boolean lowercaseExpandedTerms = true; + MultiTermQuery.RewriteMethod multiTermRewriteMethod = MultiTermQuery.CONSTANT_SCORE_AUTO_REWRITE_DEFAULT; + boolean allowLeadingWildcard = false; + boolean enablePositionIncrements = true; + + Analyzer analyzer; + String field; + int phraseSlop = 0; + float fuzzyMinSim = FuzzyQuery.defaultMinSimilarity; + int fuzzyPrefixLength = FuzzyQuery.defaultPrefixLength; + Locale locale = Locale.getDefault(); + + // the default date resolution + DateTools.Resolution dateResolution = null; + // maps field names to date resolutions + Map fieldToDateResolution = null; + + // The collator to use when determining range inclusion, + // for use when constructing RangeQuerys. + Collator rangeCollator = null; + + /** The default operator for parsing queries. + * Use {@link InvenioQueryParser#setDefaultOperator} to change it. + */ + static public final class Operator extends Parameter { + private Operator(String name) { + super(name); + } + static public final Operator OR = new Operator("OR"); + static public final Operator AND = new Operator("AND"); + } + + + /** Constructs a query parser. + * @param f the default field for query terms. + * @param a used to find terms in the query text. + * @deprecated Use {@link #InvenioQueryParser(Version, String, Analyzer)} instead + */ + public InvenioQueryParser(String f, Analyzer a) { + this(Version.LUCENE_24, f, a); + } + + /** Constructs a query parser. + * @param matchVersion Lucene version to match. See {@link above) + * @param f the default field for query terms. + * @param a used to find terms in the query text. + */ + public InvenioQueryParser(Version matchVersion, String f, Analyzer a) { + this(new FastCharStream(new StringReader(""))); + analyzer = a; + field = f; + if (matchVersion.onOrAfter(Version.LUCENE_29)) { + enablePositionIncrements = true; + } else { + enablePositionIncrements = false; + } + } + + /** Parses a query string, returning a {@link org.apache.lucene.search.Query}. + * @param query the query string to be parsed. + * @throws ParseException if the parsing fails + */ + public Query parse(String query) throws ParseException { + ReInit(new FastCharStream(new StringReader(query))); + try { + // TopLevelQuery is a Query followed by the end-of-input (EOF) + Query res = TopLevelQuery(field); + return res!=null ? res : newBooleanQuery(false); + } + catch (ParseException tme) { + // rethrow to include the original query: + ParseException e = new ParseException("Cannot parse '" +query+ "': " + tme.getMessage()); + e.initCause(tme); + throw e; + } + catch (TokenMgrError tme) { + ParseException e = new ParseException("Cannot parse '" +query+ "': " + tme.getMessage()); + e.initCause(tme); + throw e; + } + catch (BooleanQuery.TooManyClauses tmc) { + ParseException e = new ParseException("Cannot parse '" +query+ "': too many boolean clauses"); + e.initCause(tmc); + throw e; + } + } + + /** + * @return Returns the analyzer. + */ + public Analyzer getAnalyzer() { + return analyzer; + } + + /** + * @return Returns the field. + */ + public String getField() { + return field; + } + + /** + * Get the minimal similarity for fuzzy queries. + */ + public float getFuzzyMinSim() { + return fuzzyMinSim; + } + + /** + * Set the minimum similarity for fuzzy queries. + * Default is 0.5f. + */ + public void setFuzzyMinSim(float fuzzyMinSim) { + this.fuzzyMinSim = fuzzyMinSim; + } + + /** + * Get the prefix length for fuzzy queries. + * @return Returns the fuzzyPrefixLength. + */ + public int getFuzzyPrefixLength() { + return fuzzyPrefixLength; + } + + /** + * Set the prefix length for fuzzy queries. Default is 0. + * @param fuzzyPrefixLength The fuzzyPrefixLength to set. + */ + public void setFuzzyPrefixLength(int fuzzyPrefixLength) { + this.fuzzyPrefixLength = fuzzyPrefixLength; + } + + /** + * Sets the default slop for phrases. If zero, then exact phrase matches + * are required. Default value is zero. + */ + public void setPhraseSlop(int phraseSlop) { + this.phraseSlop = phraseSlop; + } + + /** + * Gets the default slop for phrases. + */ + public int getPhraseSlop() { + return phraseSlop; + } + + + /** + * Set to true to allow leading wildcard characters. + *

+ * When set, * or ? are allowed as + * the first character of a PrefixQuery and WildcardQuery. + * Note that this can produce very slow + * queries on big indexes. + *

+ * Default: false. + */ + public void setAllowLeadingWildcard(boolean allowLeadingWildcard) { + this.allowLeadingWildcard = allowLeadingWildcard; + } + + /** + * @see #setAllowLeadingWildcard(boolean) + */ + public boolean getAllowLeadingWildcard() { + return allowLeadingWildcard; + } + + /** + * Set to true to enable position increments in result query. + *

+ * When set, result phrase and multi-phrase queries will + * be aware of position increments. + * Useful when e.g. a StopFilter increases the position increment of + * the token that follows an omitted token. + *

+ * Default: false. + */ + public void setEnablePositionIncrements(boolean enable) { + this.enablePositionIncrements = enable; + } + + /** + * @see #setEnablePositionIncrements(boolean) + */ + public boolean getEnablePositionIncrements() { + return enablePositionIncrements; + } + + /** + * Sets the boolean operator of the InvenioQueryParser. + * In default mode (OR_OPERATOR) terms without any modifiers + * are considered optional: for example capital of Hungary is equal to + * capital OR of OR Hungary.
+ * In AND_OPERATOR mode terms are considered to be in conjunction: the + * above mentioned query is parsed as capital AND of AND Hungary + */ + public void setDefaultOperator(Operator op) { + this.operator = op; + } + + + /** + * Gets implicit operator setting, which will be either AND_OPERATOR + * or OR_OPERATOR. + */ + public Operator getDefaultOperator() { + return operator; + } + + + /** + * Whether terms of wildcard, prefix, fuzzy and range queries are to be automatically + * lower-cased or not. Default is true. + */ + public void setLowercaseExpandedTerms(boolean lowercaseExpandedTerms) { + this.lowercaseExpandedTerms = lowercaseExpandedTerms; + } + + + /** + * @see #setLowercaseExpandedTerms(boolean) + */ + public boolean getLowercaseExpandedTerms() { + return lowercaseExpandedTerms; + } + + /** + * @deprecated Please use {@link #setMultiTermRewriteMethod} instead. + */ + public void setUseOldRangeQuery(boolean useOldRangeQuery) { + if (useOldRangeQuery) { + setMultiTermRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE); + } else { + setMultiTermRewriteMethod(MultiTermQuery.CONSTANT_SCORE_AUTO_REWRITE_DEFAULT); + } + } + + + /** + * @deprecated Please use {@link #getMultiTermRewriteMethod} instead. + */ + public boolean getUseOldRangeQuery() { + if (getMultiTermRewriteMethod() == MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE) { + return true; + } else { + return false; + } + } + + /** + * By default InvenioQueryParser uses {@link MultiTermQuery#CONSTANT_SCORE_AUTO_REWRITE_DEFAULT} + * when creating a PrefixQuery, WildcardQuery or RangeQuery. This implementation is generally preferable because it + * a) Runs faster b) Does not have the scarcity of terms unduly influence score + * c) avoids any "TooManyBooleanClauses" exception. + * However, if your application really needs to use the + * old-fashioned BooleanQuery expansion rewriting and the above + * points are not relevant then use this to change + * the rewrite method. + */ + public void setMultiTermRewriteMethod(MultiTermQuery.RewriteMethod method) { + multiTermRewriteMethod = method; + } + + + /** + * @see #setMultiTermRewriteMethod + */ + public MultiTermQuery.RewriteMethod getMultiTermRewriteMethod() { + return multiTermRewriteMethod; + } + + /** + * Set locale used by date range parsing. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Returns current locale, allowing access by subclasses. + */ + public Locale getLocale() { + return locale; + } + + /** + * Sets the default date resolution used by RangeQueries for fields for which no + * specific date resolutions has been set. Field specific resolutions can be set + * with {@link #setDateResolution(String, DateTools.Resolution)}. + * + * @param dateResolution the default date resolution to set + */ + public void setDateResolution(DateTools.Resolution dateResolution) { + this.dateResolution = dateResolution; + } + + /** + * Sets the date resolution used by RangeQueries for a specific field. + * + * @param fieldName field for which the date resolution is to be set + * @param dateResolution date resolution to set + */ + public void setDateResolution(String fieldName, DateTools.Resolution dateResolution) { + if (fieldName == null) { + throw new IllegalArgumentException("Field cannot be null."); + } + + if (fieldToDateResolution == null) { + // lazily initialize HashMap + fieldToDateResolution = new HashMap(); + } + + fieldToDateResolution.put(fieldName, dateResolution); + } + + /** + * Returns the date resolution that is used by RangeQueries for the given field. + * Returns null, if no default or field specific date resolution has been set + * for the given field. + * + */ + public DateTools.Resolution getDateResolution(String fieldName) { + if (fieldName == null) { + throw new IllegalArgumentException("Field cannot be null."); + } + + if (fieldToDateResolution == null) { + // no field specific date resolutions set; return default date resolution instead + return this.dateResolution; + } + + DateTools.Resolution resolution = (DateTools.Resolution) fieldToDateResolution.get(fieldName); + if (resolution == null) { + // no date resolutions set for the given field; return default date resolution instead + resolution = this.dateResolution; + } + + return resolution; + } + + /** + * Sets the collator used to determine index term inclusion in ranges + * for RangeQuerys. + *

+ * WARNING: Setting the rangeCollator to a non-null + * collator using this method will cause every single index Term in the + * Field referenced by lowerTerm and/or upperTerm to be examined. + * Depending on the number of index Terms in this Field, the operation could + * be very slow. + * + * @param rc the collator to use when constructing RangeQuerys + */ + public void setRangeCollator(Collator rc) { + rangeCollator = rc; + } + + /** + * @return the collator used to determine index term inclusion in ranges + * for RangeQuerys. + */ + public Collator getRangeCollator() { + return rangeCollator; + } + + /** + * @deprecated use {@link #addClause(List, int, int, Query)} instead. + */ + protected void addClause(Vector clauses, int conj, int mods, Query q) { + addClause((List) clauses, conj, mods, q); + } + + protected void addClause(List clauses, int conj, int mods, Query q) { + boolean required, prohibited; + + // If this term is introduced by AND, make the preceding term required, + // unless it's already prohibited + if (clauses.size() > 0 && conj == CONJ_AND) { + BooleanClause c = (BooleanClause) clauses.get(clauses.size()-1); + if (!c.isProhibited()) + c.setOccur(BooleanClause.Occur.MUST); + } + + if (clauses.size() > 0 && operator == AND_OPERATOR && conj == CONJ_OR) { + // If this term is introduced by OR, make the preceding term optional, + // unless it's prohibited (that means we leave -a OR b but +a OR b-->a OR b) + // notice if the input is a OR b, first term is parsed as required; without + // this modification a OR b would parsed as +a OR b + BooleanClause c = (BooleanClause) clauses.get(clauses.size()-1); + if (!c.isProhibited()) + c.setOccur(BooleanClause.Occur.SHOULD); + } + + // We might have been passed a null query; the term might have been + // filtered away by the analyzer. + if (q == null) + return; + + if (operator == OR_OPERATOR) { + // We set REQUIRED if we're introduced by AND or +; PROHIBITED if + // introduced by NOT or -; make sure not to set both. + prohibited = (mods == MOD_NOT); + required = (mods == MOD_REQ); + if (conj == CONJ_AND && !prohibited) { + required = true; + } + } else { + // We set PROHIBITED if we're introduced by NOT or -; We set REQUIRED + // if not PROHIBITED and not introduced by OR + prohibited = (mods == MOD_NOT); + required = (!prohibited && conj != CONJ_OR); + } + if (required && !prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST)); + else if (!required && !prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.SHOULD)); + else if (!required && prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST_NOT)); + else + throw new RuntimeException("Clause cannot be both required and prohibited"); + } + + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText) throws ParseException { + // Use the analyzer to get all the tokens, and then build a TermQuery, + // PhraseQuery, or nothing based on the term count + + TokenStream source; + try { + source = analyzer.reusableTokenStream(field, new StringReader(queryText)); + source.reset(); + } catch (IOException e) { + source = analyzer.tokenStream(field, new StringReader(queryText)); + } + CachingTokenFilter buffer = new CachingTokenFilter(source); + TermAttribute termAtt = null; + PositionIncrementAttribute posIncrAtt = null; + int numTokens = 0; + + boolean success = false; + try { + buffer.reset(); + success = true; + } catch (IOException e) { + // success==false if we hit an exception + } + if (success) { + if (buffer.hasAttribute(TermAttribute.class)) { + termAtt = (TermAttribute) buffer.getAttribute(TermAttribute.class); + } + if (buffer.hasAttribute(PositionIncrementAttribute.class)) { + posIncrAtt = (PositionIncrementAttribute) buffer.getAttribute(PositionIncrementAttribute.class); + } + } + + int positionCount = 0; + boolean severalTokensAtSamePosition = false; + + boolean hasMoreTokens = false; + if (termAtt != null) { + try { + hasMoreTokens = buffer.incrementToken(); + while (hasMoreTokens) { + numTokens++; + int positionIncrement = (posIncrAtt != null) ? posIncrAtt.getPositionIncrement() : 1; + if (positionIncrement != 0) { + positionCount += positionIncrement; + } else { + severalTokensAtSamePosition = true; + } + hasMoreTokens = buffer.incrementToken(); + } + } catch (IOException e) { + // ignore + } + } + try { + // rewind the buffer stream + buffer.reset(); + + // close original stream - all tokens buffered + source.close(); + } + catch (IOException e) { + // ignore + } + + if (numTokens == 0) + return null; + else if (numTokens == 1) { + String term = null; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + return newTermQuery(new Term(field, term)); + } else { + if (severalTokensAtSamePosition) { + if (positionCount == 1) { + // no phrase query: + BooleanQuery q = newBooleanQuery(true); + for (int i = 0; i < numTokens; i++) { + String term = null; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + Query currentQuery = newTermQuery( + new Term(field, term)); + q.add(currentQuery, BooleanClause.Occur.SHOULD); + } + return q; + } + else { + // phrase query: + MultiPhraseQuery mpq = newMultiPhraseQuery(); + mpq.setSlop(phraseSlop); + List multiTerms = new ArrayList(); + int position = -1; + for (int i = 0; i < numTokens; i++) { + String term = null; + int positionIncrement = 1; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + if (posIncrAtt != null) { + positionIncrement = posIncrAtt.getPositionIncrement(); + } + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + if (positionIncrement > 0 && multiTerms.size() > 0) { + if (enablePositionIncrements) { + mpq.add((Term[])multiTerms.toArray(new Term[0]),position); + } else { + mpq.add((Term[])multiTerms.toArray(new Term[0])); + } + multiTerms.clear(); + } + position += positionIncrement; + multiTerms.add(new Term(field, term)); + } + if (enablePositionIncrements) { + mpq.add((Term[])multiTerms.toArray(new Term[0]),position); + } else { + mpq.add((Term[])multiTerms.toArray(new Term[0])); + } + return mpq; + } + } + else { + PhraseQuery pq = newPhraseQuery(); + pq.setSlop(phraseSlop); + int position = -1; + + + for (int i = 0; i < numTokens; i++) { + String term = null; + int positionIncrement = 1; + + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + if (posIncrAtt != null) { + positionIncrement = posIncrAtt.getPositionIncrement(); + } + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + if (enablePositionIncrements) { + position += positionIncrement; + pq.add(new Term(field, term),position); + } else { + pq.add(new Term(field, term)); + } + } + return pq; + } + } + } + + + + /** + * Base implementation delegates to {@link #getFieldQuery(String,String)}. + * This method may be overridden, for example, to return + * a SpanNearQuery instead of a PhraseQuery. + * + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText, int slop) + throws ParseException { + Query query = getFieldQuery(field, queryText); + + if (query instanceof PhraseQuery) { + ((PhraseQuery) query).setSlop(slop); + } + if (query instanceof MultiPhraseQuery) { + ((MultiPhraseQuery) query).setSlop(slop); + } + + return query; + } + + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getRangeQuery(String field, + String part1, + String part2, + boolean inclusive) throws ParseException + { + if (lowercaseExpandedTerms) { + part1 = part1.toLowerCase(); + part2 = part2.toLowerCase(); + } + try { + DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale); + df.setLenient(true); + Date d1 = df.parse(part1); + Date d2 = df.parse(part2); + if (inclusive) { + // The user can only specify the date, not the time, so make sure + // the time is set to the latest possible time of that date to really + // include all documents: + Calendar cal = Calendar.getInstance(locale); + cal.setTime(d2); + cal.set(Calendar.HOUR_OF_DAY, 23); + cal.set(Calendar.MINUTE, 59); + cal.set(Calendar.SECOND, 59); + cal.set(Calendar.MILLISECOND, 999); + d2 = cal.getTime(); + } + DateTools.Resolution resolution = getDateResolution(field); + if (resolution == null) { + // no default or field specific date resolution has been set, + // use deprecated DateField to maintain compatibility with + // pre-1.9 Lucene versions. + part1 = DateField.dateToString(d1); + part2 = DateField.dateToString(d2); + } else { + part1 = DateTools.dateToString(d1, resolution); + part2 = DateTools.dateToString(d2, resolution); + } + } + catch (Exception e) { } + + return newRangeQuery(field, part1, part2, inclusive); + } + + /** + * Builds a new BooleanQuery instance + * @param disableCoord disable coord + * @return new BooleanQuery instance + */ + protected BooleanQuery newBooleanQuery(boolean disableCoord) { + return new BooleanQuery(disableCoord); + } + + /** + * Builds a new BooleanClause instance + * @param q sub query + * @param occur how this clause should occur when matching documents + * @return new BooleanClause instance + */ + protected BooleanClause newBooleanClause(Query q, BooleanClause.Occur occur) { + return new BooleanClause(q, occur); + } + + /** + * Builds a new TermQuery instance + * @param term term + * @return new TermQuery instance + */ + protected Query newTermQuery(Term term){ + return new TermQuery(term); + } + + /** + * Builds a new PhraseQuery instance + * @return new PhraseQuery instance + */ + protected PhraseQuery newPhraseQuery(){ + return new PhraseQuery(); + } + + /** + * Builds a new MultiPhraseQuery instance + * @return new MultiPhraseQuery instance + */ + protected MultiPhraseQuery newMultiPhraseQuery(){ + return new MultiPhraseQuery(); + } + + /** + * Builds a new PrefixQuery instance + * @param prefix Prefix term + * @return new PrefixQuery instance + */ + protected Query newPrefixQuery(Term prefix){ + PrefixQuery query = new PrefixQuery(prefix); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Builds a new FuzzyQuery instance + * @param term Term + * @param minimumSimilarity minimum similarity + * @param prefixLength prefix length + * @return new FuzzyQuery Instance + */ + protected Query newFuzzyQuery(Term term, float minimumSimilarity, int prefixLength) { + // FuzzyQuery doesn't yet allow constant score rewrite + return new FuzzyQuery(term,minimumSimilarity,prefixLength); + } + + /** + * Builds a new TermRangeQuery instance + * @param field Field + * @param part1 min + * @param part2 max + * @param inclusive true if range is inclusive + * @return new TermRangeQuery instance + */ + protected Query newRangeQuery(String field, String part1, String part2, boolean inclusive) { + final TermRangeQuery query = new TermRangeQuery(field, part1, part2, inclusive, inclusive, rangeCollator); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Builds a new MatchAllDocsQuery instance + * @return new MatchAllDocsQuery instance + */ + protected Query newMatchAllDocsQuery() { + return new MatchAllDocsQuery(); + } + + /** + * Builds a new WildcardQuery instance + * @param t wildcard term + * @return new WildcardQuery instance + */ + protected Query newWildcardQuery(Term t) { + WildcardQuery query = new WildcardQuery(t); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + * @deprecated use {@link #getBooleanQuery(List)} instead + */ + protected Query getBooleanQuery(Vector clauses) throws ParseException { + return getBooleanQuery((List) clauses, false); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + */ + protected Query getBooleanQuery(List clauses) throws ParseException { + return getBooleanQuery(clauses, false); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * @param disableCoord true if coord scoring should be disabled. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + * @deprecated use {@link #getBooleanQuery(List, boolean)} instead + */ + protected Query getBooleanQuery(Vector clauses, boolean disableCoord) + throws ParseException + { + return getBooleanQuery((List) clauses, disableCoord); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * @param disableCoord true if coord scoring should be disabled. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + */ + protected Query getBooleanQuery(List clauses, boolean disableCoord) + throws ParseException + { + if (clauses.size()==0) { + return null; // all clause words were filtered away by the analyzer. + } + BooleanQuery query = newBooleanQuery(disableCoord); + for (int i = 0; i < clauses.size(); i++) { + query.add((BooleanClause)clauses.get(i)); + } + return query; + } + + /** + * Factory method for generating a query. Called when parser + * parses an input term token that contains one or more wildcard + * characters (? and *), but is not a prefix term token (one + * that has just a single * character at the end) + *

+ * Depending on settings, prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

+ * Can be overridden by extending classes, to provide custom handling for + * wildcard queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token that contains one or more wild card + * characters (? or *), but is not simple prefix term + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getWildcardQuery(String field, String termStr) throws ParseException + { + if ("*".equals(field)) { + if ("*".equals(termStr)) return newMatchAllDocsQuery(); + } + if (!allowLeadingWildcard && (termStr.startsWith("*") || termStr.startsWith("?"))) + throw new ParseException("'*' or '?' not allowed as first character in WildcardQuery"); + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newWildcardQuery(t); + } + + /** + * Factory method for generating a query (similar to + * {@link #getWildcardQuery}). Called when parser parses an input term + * token that uses prefix notation; that is, contains a single '*' wildcard + * character as its last character. Since this is a special case + * of generic wildcard term, and such a query can be optimized easily, + * this usually results in a different query object. + *

+ * Depending on settings, a prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

+ * Can be overridden by extending classes, to provide custom handling for + * wild card queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * (without trailing '*' character!) + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getPrefixQuery(String field, String termStr) throws ParseException + { + if (!allowLeadingWildcard && termStr.startsWith("*")) + throw new ParseException("'*' not allowed as first character in PrefixQuery"); + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newPrefixQuery(t); + } + + /** + * Factory method for generating a query (similar to + * {@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException + { + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newFuzzyQuery(t, minSimilarity, fuzzyPrefixLength); + } + + /** + * Returns a String where the escape char has been + * removed, or kept only once if there was a double escape. + * + * Supports escaped unicode characters, e. g. translates + * \\u0041 to A. + * + */ + private String discardEscapeChar(String input) throws ParseException { + // Create char array to hold unescaped char sequence + char[] output = new char[input.length()]; + + // The length of the output can be less than the input + // due to discarded escape chars. This variable holds + // the actual length of the output + int length = 0; + + // We remember whether the last processed character was + // an escape character + boolean lastCharWasEscapeChar = false; + + // The multiplier the current unicode digit must be multiplied with. + // E. g. the first digit must be multiplied with 16^3, the second with 16^2... + int codePointMultiplier = 0; + + // Used to calculate the codepoint of the escaped unicode character + int codePoint = 0; + + for (int i = 0; i < input.length(); i++) { + char curChar = input.charAt(i); + if (codePointMultiplier > 0) { + codePoint += hexToInt(curChar) * codePointMultiplier; + codePointMultiplier >>>= 4; + if (codePointMultiplier == 0) { + output[length++] = (char)codePoint; + codePoint = 0; + } + } else if (lastCharWasEscapeChar) { + if (curChar == 'u') { + // found an escaped unicode character + codePointMultiplier = 16 * 16 * 16; + } else { + // this character was escaped + output[length] = curChar; + length++; + } + lastCharWasEscapeChar = false; + } else { + if (curChar == '\u005c\u005c') { + lastCharWasEscapeChar = true; + } else { + output[length] = curChar; + length++; + } + } + } + + if (codePointMultiplier > 0) { + throw new ParseException("Truncated unicode escape sequence."); + } + + if (lastCharWasEscapeChar) { + throw new ParseException("Term can not end with escape character."); + } + + return new String(output, 0, length); + } + + /** Returns the numeric value of the hexadecimal character */ + private static final int hexToInt(char c) throws ParseException { + if ('0' <= c && c <= '9') { + return c - '0'; + } else if ('a' <= c && c <= 'f'){ + return c - 'a' + 10; + } else if ('A' <= c && c <= 'F') { + return c - 'A' + 10; + } else { + throw new ParseException("None-hex character in unicode escape sequence: " + c); + } + } + + /** + * Returns a String where those characters that InvenioQueryParser + * expects to be escaped are escaped by a preceding \. + */ + public static String escape(String s) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // These characters are part of the query syntax and must be escaped + if (c == '\u005c\u005c' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' + || c == '^' || c == '[' || c == ']' || c == '\u005c"' || c == '{' || c == '}' || c == '~' + || c == '*' || c == '?' || c == '|' || c == '&') { + sb.append('\u005c\u005c'); + } + sb.append(c); + } + return sb.toString(); + } + + /** + * Command line tool to test InvenioQueryParser, using {@link org.apache.lucene.analysis.SimpleAnalyzer}. + * Usage:
+ * java org.apache.lucene.queryParser.InvenioQueryParser <input> + */ + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.out.println("Usage: java org.apache.lucene.queryParser.InvenioQueryParser "); + System.exit(0); + } + InvenioQueryParser qp = new InvenioQueryParser(Version.LUCENE_CURRENT, "field", + new org.apache.lucene.analysis.SimpleAnalyzer()); + Query q = qp.parse(args[0]); + System.out.println(q.toString("field")); + } + +// * Query ::= ( Clause )* +// * Clause ::= ["+", "-"] [ ":"] ( | "(" Query ")" ) + final public int Conjunction() throws ParseException { + int ret = CONJ_NONE; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + case OR: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + jj_consume_token(AND); + ret = CONJ_AND; + break; + case OR: + jj_consume_token(OR); + ret = CONJ_OR; + break; + default: + jj_la1[0] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[1] = jj_gen; + ; + } + {if (true) return ret;} + throw new Error("Missing return statement in function"); + } + + final public int Modifiers() throws ParseException { + int ret = MOD_NONE; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + case PLUS: + case MINUS: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PLUS: + jj_consume_token(PLUS); + ret = MOD_REQ; + break; + case MINUS: + jj_consume_token(MINUS); + ret = MOD_NOT; + break; + case NOT: + jj_consume_token(NOT); + ret = MOD_NOT; + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[3] = jj_gen; + ; + } + {if (true) return ret;} + throw new Error("Missing return statement in function"); + } + +// This makes sure that there is no garbage after the query string + final public Query TopLevelQuery(String field) throws ParseException { + Query q; + q = Query(field); + jj_consume_token(0); + {if (true) return q;} + throw new Error("Missing return statement in function"); + } + + final public Query Query(String field) throws ParseException { + List clauses = new ArrayList(); + Query q, firstQuery=null; + int conj, mods; + mods = Modifiers(); + q = Clause(field); + addClause(clauses, CONJ_NONE, mods, q); + if (mods == MOD_NONE) + firstQuery=q; + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + case OR: + case NOT: + case PLUS: + case MINUS: + case LPAREN: + case STAR: + case QUOTED: + case QUOTED_PARTIAL: + case TERM: + case PREFIXTERM: + case WILDTERM: + case RANGEIN_START: + case RANGEEX_START: + case REGEX_TERM: + case NUMBER: + ; + break; + default: + jj_la1[4] = jj_gen; + break label_1; + } + conj = Conjunction(); + mods = Modifiers(); + q = Clause(field); + addClause(clauses, conj, mods, q); + } + if (clauses.size() == 1 && firstQuery != null) + {if (true) return firstQuery;} + else { + {if (true) return getBooleanQuery(clauses);} + } + throw new Error("Missing return statement in function"); + } + + final public Query Clause(String field) throws ParseException { + Query q; + Token fieldToken=null, boost=null; + if (jj_2_1(2)) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TERM: + fieldToken = jj_consume_token(TERM); + jj_consume_token(COLON); + field=discardEscapeChar(fieldToken.image); + break; + case STAR: + jj_consume_token(STAR); + jj_consume_token(COLON); + field="*"; + break; + default: + jj_la1[5] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } else { + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case STAR: + case QUOTED: + case QUOTED_PARTIAL: + case TERM: + case PREFIXTERM: + case WILDTERM: + case RANGEIN_START: + case RANGEEX_START: + case REGEX_TERM: + case NUMBER: + q = Term(field); + break; + case LPAREN: + jj_consume_token(LPAREN); + q = Query(field); + jj_consume_token(RPAREN); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[6] = jj_gen; + ; + } + break; + default: + jj_la1[7] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + if (boost != null) { + float f = (float)1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + q.setBoost(f); + } catch (Exception ignored) { } + } + {if (true) return q;} + throw new Error("Missing return statement in function"); + } + + final public Query Term(String field) throws ParseException { + Token term, boost=null, fuzzySlop=null, goop1, goop2; + boolean prefix = false; + boolean wildcard = false; + boolean fuzzy = false; + Query q; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case STAR: + case TERM: + case PREFIXTERM: + case WILDTERM: + case REGEX_TERM: + case NUMBER: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TERM: + term = jj_consume_token(TERM); + break; + case STAR: + term = jj_consume_token(STAR); + wildcard=true; + break; + case PREFIXTERM: + term = jj_consume_token(PREFIXTERM); + prefix=true; + break; + case WILDTERM: + term = jj_consume_token(WILDTERM); + wildcard=true; + break; + case NUMBER: + term = jj_consume_token(NUMBER); + break; + case REGEX_TERM: + term = jj_consume_token(REGEX_TERM); + break; + default: + jj_la1[8] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + fuzzy=true; + break; + default: + jj_la1[9] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + fuzzy=true; + break; + default: + jj_la1[10] = jj_gen; + ; + } + break; + default: + jj_la1[11] = jj_gen; + ; + } + String termImage=discardEscapeChar(term.image); + if (wildcard) { + q = getWildcardQuery(field, termImage); + } else if (prefix) { + q = getPrefixQuery(field, + discardEscapeChar(term.image.substring + (0, term.image.length()-1))); + } else if (fuzzy) { + float fms = fuzzyMinSim; + try { + fms = Float.valueOf(fuzzySlop.image.substring(1)).floatValue(); + } catch (Exception ignored) { } + if(fms < 0.0f || fms > 1.0f){ + {if (true) throw new ParseException("Minimum similarity for a FuzzyQuery has to be between 0.0f and 1.0f !");} + } + q = getFuzzyQuery(field, termImage,fms); + } else { + q = getFieldQuery(field, termImage); + } + break; + case RANGEIN_START: + jj_consume_token(RANGEIN_START); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEIN_GOOP: + goop1 = jj_consume_token(RANGEIN_GOOP); + break; + case RANGEIN_QUOTED: + goop1 = jj_consume_token(RANGEIN_QUOTED); + break; + default: + jj_la1[12] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEIN_TO: + jj_consume_token(RANGEIN_TO); + break; + default: + jj_la1[13] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEIN_GOOP: + goop2 = jj_consume_token(RANGEIN_GOOP); + break; + case RANGEIN_QUOTED: + goop2 = jj_consume_token(RANGEIN_QUOTED); + break; + default: + jj_la1[14] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RANGEIN_END); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[15] = jj_gen; + ; + } + if (goop1.kind == RANGEIN_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } + if (goop2.kind == RANGEIN_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } + q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), true); + break; + case RANGEEX_START: + jj_consume_token(RANGEEX_START); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEEX_GOOP: + goop1 = jj_consume_token(RANGEEX_GOOP); + break; + case RANGEEX_QUOTED: + goop1 = jj_consume_token(RANGEEX_QUOTED); + break; + default: + jj_la1[16] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEEX_TO: + jj_consume_token(RANGEEX_TO); + break; + default: + jj_la1[17] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case RANGEEX_GOOP: + goop2 = jj_consume_token(RANGEEX_GOOP); + break; + case RANGEEX_QUOTED: + goop2 = jj_consume_token(RANGEEX_QUOTED); + break; + default: + jj_la1[18] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RANGEEX_END); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[19] = jj_gen; + ; + } + if (goop1.kind == RANGEEX_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } + if (goop2.kind == RANGEEX_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } + + q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), false); + break; + case QUOTED: + term = jj_consume_token(QUOTED); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + break; + default: + jj_la1[20] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[21] = jj_gen; + ; + } + int s = phraseSlop; + + if (fuzzySlop != null) { + try { + s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { } + } + q = getFieldQuery(field, discardEscapeChar(term.image.substring(1, term.image.length()-1)), s); + break; + case QUOTED_PARTIAL: + term = jj_consume_token(QUOTED_PARTIAL); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FUZZY_SLOP: + fuzzySlop = jj_consume_token(FUZZY_SLOP); + break; + default: + jj_la1[22] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CARAT: + jj_consume_token(CARAT); + boost = jj_consume_token(NUMBER); + break; + default: + jj_la1[23] = jj_gen; + ; + } + int partialSlop = 2; + + if (fuzzySlop != null) { + try { + partialSlop = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { + partialSlop = 2; + } + } + q = getFieldQuery(field, discardEscapeChar(term.image.substring(1, term.image.length()-1)), partialSlop); + break; + default: + jj_la1[24] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + if (boost != null) { + float f = (float) 1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + } + catch (Exception ignored) { + /* Should this be handled somehow? (defaults to "no boost", if + * boost number is invalid) + */ + } + + // avoid boosting null queries, such as those caused by stop words + if (q != null) { + q.setBoost(f); + } + } + {if (true) return q;} + throw new Error("Missing return statement in function"); + } + + private boolean jj_2_1(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_1(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(0, xla); } + } + + private boolean jj_3R_3() { + if (jj_scan_token(STAR)) return true; + if (jj_scan_token(COLON)) return true; + return false; + } + + private boolean jj_3R_2() { + if (jj_scan_token(TERM)) return true; + if (jj_scan_token(COLON)) return true; + return false; + } + + private boolean jj_3_1() { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_2()) { + jj_scanpos = xsp; + if (jj_3R_3()) return true; + } + return false; + } + + /** Generated Token Manager. */ + public InvenioQueryParserTokenManager token_source; + /** Current token. */ + public Token token; + /** Next token. */ + public Token jj_nt; + private int jj_ntk; + private Token jj_scanpos, jj_lastpos; + private int jj_la; + private int jj_gen; + final private int[] jj_la1 = new int[25]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static { + jj_la1_init_0(); + jj_la1_init_1(); + } + private static void jj_la1_init_0() { + jj_la1_0 = new int[] {0xc00,0xc00,0x7000,0x7000,0x3f74fc00,0x440000,0x80000,0x3f748000,0x33440000,0x800000,0x800000,0x80000,0x0,0x40000000,0x0,0x80000,0x0,0x0,0x0,0x80000,0x800000,0x80000,0x800000,0x80000,0x3f740000,}; + } + private static void jj_la1_init_1() { + jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3,0x0,0x3,0x0,0x30,0x4,0x30,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + final private JJCalls[] jj_2_rtns = new JJCalls[1]; + private boolean jj_rescan = false; + private int jj_gc = 0; + + /** Constructor with user supplied CharStream. */ + protected InvenioQueryParser(CharStream stream) { + token_source = new InvenioQueryParserTokenManager(stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 25; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + /** Reinitialise. */ + public void ReInit(CharStream stream) { + token_source.ReInit(stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 25; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + /** Constructor with generated Token Manager. */ + protected InvenioQueryParser(InvenioQueryParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 25; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + /** Reinitialise. */ + public void ReInit(InvenioQueryParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 25; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + if (++jj_gc > 100) { + jj_gc = 0; + for (int i = 0; i < jj_2_rtns.length; i++) { + JJCalls c = jj_2_rtns[i]; + while (c != null) { + if (c.gen < jj_gen) c.first = null; + c = c.next; + } + } + } + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + static private final class LookaheadSuccess extends java.lang.Error { } + final private LookaheadSuccess jj_ls = new LookaheadSuccess(); + private boolean jj_scan_token(int kind) { + if (jj_scanpos == jj_lastpos) { + jj_la--; + if (jj_scanpos.next == null) { + jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken(); + } else { + jj_lastpos = jj_scanpos = jj_scanpos.next; + } + } else { + jj_scanpos = jj_scanpos.next; + } + if (jj_rescan) { + int i = 0; Token tok = token; + while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; } + if (tok != null) jj_add_error_token(kind, i); + } + if (jj_scanpos.kind != kind) return true; + if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls; + return false; + } + + +/** Get the next Token. */ + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + +/** Get the specific Token. */ + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.List jj_expentries = new java.util.ArrayList(); + private int[] jj_expentry; + private int jj_kind = -1; + private int[] jj_lasttokens = new int[100]; + private int jj_endpos; + + private void jj_add_error_token(int kind, int pos) { + if (pos >= 100) return; + if (pos == jj_endpos + 1) { + jj_lasttokens[jj_endpos++] = kind; + } else if (jj_endpos != 0) { + jj_expentry = new int[jj_endpos]; + for (int i = 0; i < jj_endpos; i++) { + jj_expentry[i] = jj_lasttokens[i]; + } + jj_entries_loop: for (java.util.Iterator it = jj_expentries.iterator(); it.hasNext();) { + int[] oldentry = (int[])(it.next()); + if (oldentry.length == jj_expentry.length) { + for (int i = 0; i < jj_expentry.length; i++) { + if (oldentry[i] != jj_expentry[i]) { + continue jj_entries_loop; + } + } + jj_expentries.add(jj_expentry); + break jj_entries_loop; + } + } + if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind; + } + } + + /** Generate ParseException. */ + public ParseException generateParseException() { + jj_expentries.clear(); + boolean[] la1tokens = new boolean[38]; + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 25; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1< jj_gen) { + jj_la = p.arg; jj_lastpos = jj_scanpos = p.first; + switch (i) { + case 0: jj_3_1(); break; + } + } + p = p.next; + } while (p != null); + } catch(LookaheadSuccess ls) { } + } + jj_rescan = false; + } + + private void jj_save(int index, int xla) { + JJCalls p = jj_2_rtns[index]; + while (p.gen > jj_gen) { + if (p.next == null) { p = p.next = new JJCalls(); break; } + p = p.next; + } + p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + +} diff --git a/src/java/org/apache/lucene/queryParser/InvenioQueryParser.jj b/src/java/org/apache/lucene/queryParser/InvenioQueryParser.jj new file mode 100644 index 000000000..9eff146ad --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/InvenioQueryParser.jj @@ -0,0 +1,1493 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +options { + STATIC=false; + JAVA_UNICODE_ESCAPE=true; + USER_CHAR_STREAM=true; +} + +PARSER_BEGIN(InvenioQueryParser) + +package org.apache.lucene.queryParser; + +import java.io.IOException; +import java.io.StringReader; +import java.text.Collator; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Vector; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.CachingTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.DateField; +import org.apache.lucene.document.DateTools; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.Parameter; +import org.apache.lucene.util.Version; + +/** + * This class is generated by JavaCC. The most important method is + * {@link #parse(String)}. + * + * The syntax for query strings is as follows: + * A Query is a series of clauses. + * A clause may be prefixed by: + *

    + *
  • a plus (+) or a minus (-) sign, indicating + * that the clause is required or prohibited respectively; or + *
  • a term followed by a colon, indicating the field to be searched. + * This enables one to construct queries which search multiple fields. + *
+ * + * A clause may be either: + *
    + *
  • a term, indicating all the documents that contain this term; or + *
  • a nested query, enclosed in parentheses. Note that this may be used + * with a +/- prefix to require any of a set of + * terms. + *
+ * + * Thus, in BNF, the query grammar is: + *
+ *   Query  ::= ( Clause )*
+ *   Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")" )
+ * 
+ * + *

+ * Examples of appropriately formatted queries can be found in the query syntax + * documentation. + *

+ * + *

+ * In {@link TermRangeQuery}s, InvenioQueryParser tries to detect date values, e.g. + * date:[6/1/2005 TO 6/4/2005] produces a range query that searches + * for "date" fields between 2005-06-01 and 2005-06-04. Note that the format + * of the accepted input depends on {@link #setLocale(Locale) the locale}. + * By default a date is converted into a search term using the deprecated + * {@link DateField} for compatibility reasons. + * To use the new {@link DateTools} to convert dates, a + * {@link org.apache.lucene.document.DateTools.Resolution} has to be set. + *

+ *

+ * The date resolution that shall be used for RangeQueries can be set + * using {@link #setDateResolution(DateTools.Resolution)} + * or {@link #setDateResolution(String, DateTools.Resolution)}. The former + * sets the default date resolution for all fields, whereas the latter can + * be used to set field specific date resolutions. Field specific date + * resolutions take, if set, precedence over the default date resolution. + *

+ *

+ * If you use neither {@link DateField} nor {@link DateTools} in your + * index, you can create your own + * query parser that inherits InvenioQueryParser and overwrites + * {@link #getRangeQuery(String, String, String, boolean)} to + * use a different method for date conversion. + *

+ * + *

Note that InvenioQueryParser is not thread-safe.

+ * + *

NOTE: there is a new InvenioQueryParser in contrib, which matches + * the same syntax as this class, but is more modular, + * enabling substantial customization to how a query is created. + * + * + *

NOTE: You must specify the required {@link Version} + * compatibility when creating InvenioQueryParser: + *

+ */ +public class InvenioQueryParser { + + private static final int CONJ_NONE = 0; + private static final int CONJ_AND = 1; + private static final int CONJ_OR = 2; + + private static final int MOD_NONE = 0; + private static final int MOD_NOT = 10; + private static final int MOD_REQ = 11; + private static final int MOD_SECOND = 12; + + // make it possible to call setDefaultOperator() without accessing + // the nested class: + /** Alternative form of InvenioQueryParser.Operator.AND */ + public static final Operator AND_OPERATOR = Operator.AND; + /** Alternative form of InvenioQueryParser.Operator.OR */ + public static final Operator OR_OPERATOR = Operator.OR; + + /** The actual operator that parser uses to combine query terms */ + private Operator operator = OR_OPERATOR; + + boolean lowercaseExpandedTerms = true; + MultiTermQuery.RewriteMethod multiTermRewriteMethod = MultiTermQuery.CONSTANT_SCORE_AUTO_REWRITE_DEFAULT; + boolean allowLeadingWildcard = false; + boolean enablePositionIncrements = true; + + Analyzer analyzer; + String field; + int phraseSlop = 0; + float fuzzyMinSim = FuzzyQuery.defaultMinSimilarity; + int fuzzyPrefixLength = FuzzyQuery.defaultPrefixLength; + Locale locale = Locale.getDefault(); + + // the default date resolution + DateTools.Resolution dateResolution = null; + // maps field names to date resolutions + Map fieldToDateResolution = null; + + // The collator to use when determining range inclusion, + // for use when constructing RangeQuerys. + Collator rangeCollator = null; + + /** The default operator for parsing queries. + * Use {@link InvenioQueryParser#setDefaultOperator} to change it. + */ + static public final class Operator extends Parameter { + private Operator(String name) { + super(name); + } + static public final Operator OR = new Operator("OR"); + static public final Operator AND = new Operator("AND"); + } + + + /** Constructs a query parser. + * @param f the default field for query terms. + * @param a used to find terms in the query text. + * @deprecated Use {@link #InvenioQueryParser(Version, String, Analyzer)} instead + */ + public InvenioQueryParser(String f, Analyzer a) { + this(Version.LUCENE_24, f, a); + } + + /** Constructs a query parser. + * @param matchVersion Lucene version to match. See {@link above) + * @param f the default field for query terms. + * @param a used to find terms in the query text. + */ + public InvenioQueryParser(Version matchVersion, String f, Analyzer a) { + this(new FastCharStream(new StringReader(""))); + analyzer = a; + field = f; + if (matchVersion.onOrAfter(Version.LUCENE_29)) { + enablePositionIncrements = true; + } else { + enablePositionIncrements = false; + } + } + + /** Parses a query string, returning a {@link org.apache.lucene.search.Query}. + * @param query the query string to be parsed. + * @throws ParseException if the parsing fails + */ + public Query parse(String query) throws ParseException { + ReInit(new FastCharStream(new StringReader(query))); + try { + // TopLevelQuery is a Query followed by the end-of-input (EOF) + Query res = TopLevelQuery(field); + return res!=null ? res : newBooleanQuery(false); + } + catch (ParseException tme) { + // rethrow to include the original query: + ParseException e = new ParseException("Cannot parse '" +query+ "': " + tme.getMessage()); + e.initCause(tme); + throw e; + } + catch (TokenMgrError tme) { + ParseException e = new ParseException("Cannot parse '" +query+ "': " + tme.getMessage()); + e.initCause(tme); + throw e; + } + catch (BooleanQuery.TooManyClauses tmc) { + ParseException e = new ParseException("Cannot parse '" +query+ "': too many boolean clauses"); + e.initCause(tmc); + throw e; + } + } + + /** + * @return Returns the analyzer. + */ + public Analyzer getAnalyzer() { + return analyzer; + } + + /** + * @return Returns the field. + */ + public String getField() { + return field; + } + + /** + * Get the minimal similarity for fuzzy queries. + */ + public float getFuzzyMinSim() { + return fuzzyMinSim; + } + + /** + * Set the minimum similarity for fuzzy queries. + * Default is 0.5f. + */ + public void setFuzzyMinSim(float fuzzyMinSim) { + this.fuzzyMinSim = fuzzyMinSim; + } + + /** + * Get the prefix length for fuzzy queries. + * @return Returns the fuzzyPrefixLength. + */ + public int getFuzzyPrefixLength() { + return fuzzyPrefixLength; + } + + /** + * Set the prefix length for fuzzy queries. Default is 0. + * @param fuzzyPrefixLength The fuzzyPrefixLength to set. + */ + public void setFuzzyPrefixLength(int fuzzyPrefixLength) { + this.fuzzyPrefixLength = fuzzyPrefixLength; + } + + /** + * Sets the default slop for phrases. If zero, then exact phrase matches + * are required. Default value is zero. + */ + public void setPhraseSlop(int phraseSlop) { + this.phraseSlop = phraseSlop; + } + + /** + * Gets the default slop for phrases. + */ + public int getPhraseSlop() { + return phraseSlop; + } + + + /** + * Set to true to allow leading wildcard characters. + *

+ * When set, * or ? are allowed as + * the first character of a PrefixQuery and WildcardQuery. + * Note that this can produce very slow + * queries on big indexes. + *

+ * Default: false. + */ + public void setAllowLeadingWildcard(boolean allowLeadingWildcard) { + this.allowLeadingWildcard = allowLeadingWildcard; + } + + /** + * @see #setAllowLeadingWildcard(boolean) + */ + public boolean getAllowLeadingWildcard() { + return allowLeadingWildcard; + } + + /** + * Set to true to enable position increments in result query. + *

+ * When set, result phrase and multi-phrase queries will + * be aware of position increments. + * Useful when e.g. a StopFilter increases the position increment of + * the token that follows an omitted token. + *

+ * Default: false. + */ + public void setEnablePositionIncrements(boolean enable) { + this.enablePositionIncrements = enable; + } + + /** + * @see #setEnablePositionIncrements(boolean) + */ + public boolean getEnablePositionIncrements() { + return enablePositionIncrements; + } + + /** + * Sets the boolean operator of the InvenioQueryParser. + * In default mode (OR_OPERATOR) terms without any modifiers + * are considered optional: for example capital of Hungary is equal to + * capital OR of OR Hungary.
+ * In AND_OPERATOR mode terms are considered to be in conjunction: the + * above mentioned query is parsed as capital AND of AND Hungary + */ + public void setDefaultOperator(Operator op) { + this.operator = op; + } + + + /** + * Gets implicit operator setting, which will be either AND_OPERATOR + * or OR_OPERATOR. + */ + public Operator getDefaultOperator() { + return operator; + } + + + /** + * Whether terms of wildcard, prefix, fuzzy and range queries are to be automatically + * lower-cased or not. Default is true. + */ + public void setLowercaseExpandedTerms(boolean lowercaseExpandedTerms) { + this.lowercaseExpandedTerms = lowercaseExpandedTerms; + } + + + /** + * @see #setLowercaseExpandedTerms(boolean) + */ + public boolean getLowercaseExpandedTerms() { + return lowercaseExpandedTerms; + } + + /** + * @deprecated Please use {@link #setMultiTermRewriteMethod} instead. + */ + public void setUseOldRangeQuery(boolean useOldRangeQuery) { + if (useOldRangeQuery) { + setMultiTermRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE); + } else { + setMultiTermRewriteMethod(MultiTermQuery.CONSTANT_SCORE_AUTO_REWRITE_DEFAULT); + } + } + + + /** + * @deprecated Please use {@link #getMultiTermRewriteMethod} instead. + */ + public boolean getUseOldRangeQuery() { + if (getMultiTermRewriteMethod() == MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE) { + return true; + } else { + return false; + } + } + + /** + * By default InvenioQueryParser uses {@link MultiTermQuery#CONSTANT_SCORE_AUTO_REWRITE_DEFAULT} + * when creating a PrefixQuery, WildcardQuery or RangeQuery. This implementation is generally preferable because it + * a) Runs faster b) Does not have the scarcity of terms unduly influence score + * c) avoids any "TooManyBooleanClauses" exception. + * However, if your application really needs to use the + * old-fashioned BooleanQuery expansion rewriting and the above + * points are not relevant then use this to change + * the rewrite method. + */ + public void setMultiTermRewriteMethod(MultiTermQuery.RewriteMethod method) { + multiTermRewriteMethod = method; + } + + + /** + * @see #setMultiTermRewriteMethod + */ + public MultiTermQuery.RewriteMethod getMultiTermRewriteMethod() { + return multiTermRewriteMethod; + } + + /** + * Set locale used by date range parsing. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Returns current locale, allowing access by subclasses. + */ + public Locale getLocale() { + return locale; + } + + /** + * Sets the default date resolution used by RangeQueries for fields for which no + * specific date resolutions has been set. Field specific resolutions can be set + * with {@link #setDateResolution(String, DateTools.Resolution)}. + * + * @param dateResolution the default date resolution to set + */ + public void setDateResolution(DateTools.Resolution dateResolution) { + this.dateResolution = dateResolution; + } + + /** + * Sets the date resolution used by RangeQueries for a specific field. + * + * @param fieldName field for which the date resolution is to be set + * @param dateResolution date resolution to set + */ + public void setDateResolution(String fieldName, DateTools.Resolution dateResolution) { + if (fieldName == null) { + throw new IllegalArgumentException("Field cannot be null."); + } + + if (fieldToDateResolution == null) { + // lazily initialize HashMap + fieldToDateResolution = new HashMap(); + } + + fieldToDateResolution.put(fieldName, dateResolution); + } + + /** + * Returns the date resolution that is used by RangeQueries for the given field. + * Returns null, if no default or field specific date resolution has been set + * for the given field. + * + */ + public DateTools.Resolution getDateResolution(String fieldName) { + if (fieldName == null) { + throw new IllegalArgumentException("Field cannot be null."); + } + + if (fieldToDateResolution == null) { + // no field specific date resolutions set; return default date resolution instead + return this.dateResolution; + } + + DateTools.Resolution resolution = (DateTools.Resolution) fieldToDateResolution.get(fieldName); + if (resolution == null) { + // no date resolutions set for the given field; return default date resolution instead + resolution = this.dateResolution; + } + + return resolution; + } + + /** + * Sets the collator used to determine index term inclusion in ranges + * for RangeQuerys. + *

+ * WARNING: Setting the rangeCollator to a non-null + * collator using this method will cause every single index Term in the + * Field referenced by lowerTerm and/or upperTerm to be examined. + * Depending on the number of index Terms in this Field, the operation could + * be very slow. + * + * @param rc the collator to use when constructing RangeQuerys + */ + public void setRangeCollator(Collator rc) { + rangeCollator = rc; + } + + /** + * @return the collator used to determine index term inclusion in ranges + * for RangeQuerys. + */ + public Collator getRangeCollator() { + return rangeCollator; + } + + /** + * @deprecated use {@link #addClause(List, int, int, Query)} instead. + */ + protected void addClause(Vector clauses, int conj, int mods, Query q) { + addClause((List) clauses, conj, mods, q); + } + + protected void addClause(List clauses, int conj, int mods, Query q) { + boolean required, prohibited; + + // If this term is introduced by AND, make the preceding term required, + // unless it's already prohibited + if (clauses.size() > 0 && conj == CONJ_AND) { + BooleanClause c = (BooleanClause) clauses.get(clauses.size()-1); + if (!c.isProhibited()) + c.setOccur(BooleanClause.Occur.MUST); + } + + if (clauses.size() > 0 && operator == AND_OPERATOR && conj == CONJ_OR) { + // If this term is introduced by OR, make the preceding term optional, + // unless it's prohibited (that means we leave -a OR b but +a OR b-->a OR b) + // notice if the input is a OR b, first term is parsed as required; without + // this modification a OR b would parsed as +a OR b + BooleanClause c = (BooleanClause) clauses.get(clauses.size()-1); + if (!c.isProhibited()) + c.setOccur(BooleanClause.Occur.SHOULD); + } + + // We might have been passed a null query; the term might have been + // filtered away by the analyzer. + if (q == null) + return; + + if (operator == OR_OPERATOR) { + // We set REQUIRED if we're introduced by AND or +; PROHIBITED if + // introduced by NOT or -; make sure not to set both. + prohibited = (mods == MOD_NOT); + required = (mods == MOD_REQ); + if (conj == CONJ_AND && !prohibited) { + required = true; + } + } else { + // We set PROHIBITED if we're introduced by NOT or -; We set REQUIRED + // if not PROHIBITED and not introduced by OR + prohibited = (mods == MOD_NOT); + required = (!prohibited && conj != CONJ_OR); + } + if (required && !prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST)); + else if (!required && !prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.SHOULD)); + else if (!required && prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST_NOT)); + else + throw new RuntimeException("Clause cannot be both required and prohibited"); + } + + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText) throws ParseException { + // Use the analyzer to get all the tokens, and then build a TermQuery, + // PhraseQuery, or nothing based on the term count + + TokenStream source; + try { + source = analyzer.reusableTokenStream(field, new StringReader(queryText)); + source.reset(); + } catch (IOException e) { + source = analyzer.tokenStream(field, new StringReader(queryText)); + } + CachingTokenFilter buffer = new CachingTokenFilter(source); + TermAttribute termAtt = null; + PositionIncrementAttribute posIncrAtt = null; + int numTokens = 0; + + boolean success = false; + try { + buffer.reset(); + success = true; + } catch (IOException e) { + // success==false if we hit an exception + } + if (success) { + if (buffer.hasAttribute(TermAttribute.class)) { + termAtt = (TermAttribute) buffer.getAttribute(TermAttribute.class); + } + if (buffer.hasAttribute(PositionIncrementAttribute.class)) { + posIncrAtt = (PositionIncrementAttribute) buffer.getAttribute(PositionIncrementAttribute.class); + } + } + + int positionCount = 0; + boolean severalTokensAtSamePosition = false; + + boolean hasMoreTokens = false; + if (termAtt != null) { + try { + hasMoreTokens = buffer.incrementToken(); + while (hasMoreTokens) { + numTokens++; + int positionIncrement = (posIncrAtt != null) ? posIncrAtt.getPositionIncrement() : 1; + if (positionIncrement != 0) { + positionCount += positionIncrement; + } else { + severalTokensAtSamePosition = true; + } + hasMoreTokens = buffer.incrementToken(); + } + } catch (IOException e) { + // ignore + } + } + try { + // rewind the buffer stream + buffer.reset(); + + // close original stream - all tokens buffered + source.close(); + } + catch (IOException e) { + // ignore + } + + if (numTokens == 0) + return null; + else if (numTokens == 1) { + String term = null; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + return newTermQuery(new Term(field, term)); + } else { + if (severalTokensAtSamePosition) { + if (positionCount == 1) { + // no phrase query: + BooleanQuery q = newBooleanQuery(true); + for (int i = 0; i < numTokens; i++) { + String term = null; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + Query currentQuery = newTermQuery( + new Term(field, term)); + q.add(currentQuery, BooleanClause.Occur.SHOULD); + } + return q; + } + else { + // phrase query: + MultiPhraseQuery mpq = newMultiPhraseQuery(); + mpq.setSlop(phraseSlop); + List multiTerms = new ArrayList(); + int position = -1; + for (int i = 0; i < numTokens; i++) { + String term = null; + int positionIncrement = 1; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + if (posIncrAtt != null) { + positionIncrement = posIncrAtt.getPositionIncrement(); + } + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + if (positionIncrement > 0 && multiTerms.size() > 0) { + if (enablePositionIncrements) { + mpq.add((Term[])multiTerms.toArray(new Term[0]),position); + } else { + mpq.add((Term[])multiTerms.toArray(new Term[0])); + } + multiTerms.clear(); + } + position += positionIncrement; + multiTerms.add(new Term(field, term)); + } + if (enablePositionIncrements) { + mpq.add((Term[])multiTerms.toArray(new Term[0]),position); + } else { + mpq.add((Term[])multiTerms.toArray(new Term[0])); + } + return mpq; + } + } + else { + PhraseQuery pq = newPhraseQuery(); + pq.setSlop(phraseSlop); + int position = -1; + + + for (int i = 0; i < numTokens; i++) { + String term = null; + int positionIncrement = 1; + + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + if (posIncrAtt != null) { + positionIncrement = posIncrAtt.getPositionIncrement(); + } + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + if (enablePositionIncrements) { + position += positionIncrement; + pq.add(new Term(field, term),position); + } else { + pq.add(new Term(field, term)); + } + } + return pq; + } + } + } + + + + /** + * Base implementation delegates to {@link #getFieldQuery(String,String)}. + * This method may be overridden, for example, to return + * a SpanNearQuery instead of a PhraseQuery. + * + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText, int slop) + throws ParseException { + Query query = getFieldQuery(field, queryText); + + if (query instanceof PhraseQuery) { + ((PhraseQuery) query).setSlop(slop); + } + if (query instanceof MultiPhraseQuery) { + ((MultiPhraseQuery) query).setSlop(slop); + } + + return query; + } + + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getRangeQuery(String field, + String part1, + String part2, + boolean inclusive) throws ParseException + { + if (lowercaseExpandedTerms) { + part1 = part1.toLowerCase(); + part2 = part2.toLowerCase(); + } + try { + DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale); + df.setLenient(true); + Date d1 = df.parse(part1); + Date d2 = df.parse(part2); + if (inclusive) { + // The user can only specify the date, not the time, so make sure + // the time is set to the latest possible time of that date to really + // include all documents: + Calendar cal = Calendar.getInstance(locale); + cal.setTime(d2); + cal.set(Calendar.HOUR_OF_DAY, 23); + cal.set(Calendar.MINUTE, 59); + cal.set(Calendar.SECOND, 59); + cal.set(Calendar.MILLISECOND, 999); + d2 = cal.getTime(); + } + DateTools.Resolution resolution = getDateResolution(field); + if (resolution == null) { + // no default or field specific date resolution has been set, + // use deprecated DateField to maintain compatibility with + // pre-1.9 Lucene versions. + part1 = DateField.dateToString(d1); + part2 = DateField.dateToString(d2); + } else { + part1 = DateTools.dateToString(d1, resolution); + part2 = DateTools.dateToString(d2, resolution); + } + } + catch (Exception e) { } + + return newRangeQuery(field, part1, part2, inclusive); + } + + /** + * Builds a new BooleanQuery instance + * @param disableCoord disable coord + * @return new BooleanQuery instance + */ + protected BooleanQuery newBooleanQuery(boolean disableCoord) { + return new BooleanQuery(disableCoord); + } + + /** + * Builds a new BooleanClause instance + * @param q sub query + * @param occur how this clause should occur when matching documents + * @return new BooleanClause instance + */ + protected BooleanClause newBooleanClause(Query q, BooleanClause.Occur occur) { + return new BooleanClause(q, occur); + } + + /** + * Builds a new TermQuery instance + * @param term term + * @return new TermQuery instance + */ + protected Query newTermQuery(Term term){ + return new TermQuery(term); + } + + /** + * Builds a new PhraseQuery instance + * @return new PhraseQuery instance + */ + protected PhraseQuery newPhraseQuery(){ + return new PhraseQuery(); + } + + /** + * Builds a new MultiPhraseQuery instance + * @return new MultiPhraseQuery instance + */ + protected MultiPhraseQuery newMultiPhraseQuery(){ + return new MultiPhraseQuery(); + } + + /** + * Builds a new PrefixQuery instance + * @param prefix Prefix term + * @return new PrefixQuery instance + */ + protected Query newPrefixQuery(Term prefix){ + PrefixQuery query = new PrefixQuery(prefix); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Builds a new FuzzyQuery instance + * @param term Term + * @param minimumSimilarity minimum similarity + * @param prefixLength prefix length + * @return new FuzzyQuery Instance + */ + protected Query newFuzzyQuery(Term term, float minimumSimilarity, int prefixLength) { + // FuzzyQuery doesn't yet allow constant score rewrite + return new FuzzyQuery(term,minimumSimilarity,prefixLength); + } + + /** + * Builds a new TermRangeQuery instance + * @param field Field + * @param part1 min + * @param part2 max + * @param inclusive true if range is inclusive + * @return new TermRangeQuery instance + */ + protected Query newRangeQuery(String field, String part1, String part2, boolean inclusive) { + final TermRangeQuery query = new TermRangeQuery(field, part1, part2, inclusive, inclusive, rangeCollator); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Builds a new MatchAllDocsQuery instance + * @return new MatchAllDocsQuery instance + */ + protected Query newMatchAllDocsQuery() { + return new MatchAllDocsQuery(); + } + + /** + * Builds a new WildcardQuery instance + * @param t wildcard term + * @return new WildcardQuery instance + */ + protected Query newWildcardQuery(Term t) { + WildcardQuery query = new WildcardQuery(t); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + * @deprecated use {@link #getBooleanQuery(List)} instead + */ + protected Query getBooleanQuery(Vector clauses) throws ParseException { + return getBooleanQuery((List) clauses, false); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + */ + protected Query getBooleanQuery(List clauses) throws ParseException { + return getBooleanQuery(clauses, false); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * @param disableCoord true if coord scoring should be disabled. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + * @deprecated use {@link #getBooleanQuery(List, boolean)} instead + */ + protected Query getBooleanQuery(Vector clauses, boolean disableCoord) + throws ParseException + { + return getBooleanQuery((List) clauses, disableCoord); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * @param disableCoord true if coord scoring should be disabled. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + */ + protected Query getBooleanQuery(List clauses, boolean disableCoord) + throws ParseException + { + if (clauses.size()==0) { + return null; // all clause words were filtered away by the analyzer. + } + BooleanQuery query = newBooleanQuery(disableCoord); + for (int i = 0; i < clauses.size(); i++) { + query.add((BooleanClause)clauses.get(i)); + } + return query; + } + + /** + * Factory method for generating a query. Called when parser + * parses an input term token that contains one or more wildcard + * characters (? and *), but is not a prefix term token (one + * that has just a single * character at the end) + *

+ * Depending on settings, prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

+ * Can be overridden by extending classes, to provide custom handling for + * wildcard queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token that contains one or more wild card + * characters (? or *), but is not simple prefix term + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getWildcardQuery(String field, String termStr) throws ParseException + { + if ("*".equals(field)) { + if ("*".equals(termStr)) return newMatchAllDocsQuery(); + } + if (!allowLeadingWildcard && (termStr.startsWith("*") || termStr.startsWith("?"))) + throw new ParseException("'*' or '?' not allowed as first character in WildcardQuery"); + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newWildcardQuery(t); + } + + /** + * Factory method for generating a query (similar to + * {@link #getWildcardQuery}). Called when parser parses an input term + * token that uses prefix notation; that is, contains a single '*' wildcard + * character as its last character. Since this is a special case + * of generic wildcard term, and such a query can be optimized easily, + * this usually results in a different query object. + *

+ * Depending on settings, a prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

+ * Can be overridden by extending classes, to provide custom handling for + * wild card queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * (without trailing '*' character!) + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getPrefixQuery(String field, String termStr) throws ParseException + { + if (!allowLeadingWildcard && termStr.startsWith("*")) + throw new ParseException("'*' not allowed as first character in PrefixQuery"); + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newPrefixQuery(t); + } + + /** + * Factory method for generating a query (similar to + * {@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException + { + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newFuzzyQuery(t, minSimilarity, fuzzyPrefixLength); + } + + /** + * Returns a String where the escape char has been + * removed, or kept only once if there was a double escape. + * + * Supports escaped unicode characters, e. g. translates + * \\u0041 to A. + * + */ + private String discardEscapeChar(String input) throws ParseException { + // Create char array to hold unescaped char sequence + char[] output = new char[input.length()]; + + // The length of the output can be less than the input + // due to discarded escape chars. This variable holds + // the actual length of the output + int length = 0; + + // We remember whether the last processed character was + // an escape character + boolean lastCharWasEscapeChar = false; + + // The multiplier the current unicode digit must be multiplied with. + // E. g. the first digit must be multiplied with 16^3, the second with 16^2... + int codePointMultiplier = 0; + + // Used to calculate the codepoint of the escaped unicode character + int codePoint = 0; + + for (int i = 0; i < input.length(); i++) { + char curChar = input.charAt(i); + if (codePointMultiplier > 0) { + codePoint += hexToInt(curChar) * codePointMultiplier; + codePointMultiplier >>>= 4; + if (codePointMultiplier == 0) { + output[length++] = (char)codePoint; + codePoint = 0; + } + } else if (lastCharWasEscapeChar) { + if (curChar == 'u') { + // found an escaped unicode character + codePointMultiplier = 16 * 16 * 16; + } else { + // this character was escaped + output[length] = curChar; + length++; + } + lastCharWasEscapeChar = false; + } else { + if (curChar == '\\') { + lastCharWasEscapeChar = true; + } else { + output[length] = curChar; + length++; + } + } + } + + if (codePointMultiplier > 0) { + throw new ParseException("Truncated unicode escape sequence."); + } + + if (lastCharWasEscapeChar) { + throw new ParseException("Term can not end with escape character."); + } + + return new String(output, 0, length); + } + + /** Returns the numeric value of the hexadecimal character */ + private static final int hexToInt(char c) throws ParseException { + if ('0' <= c && c <= '9') { + return c - '0'; + } else if ('a' <= c && c <= 'f'){ + return c - 'a' + 10; + } else if ('A' <= c && c <= 'F') { + return c - 'A' + 10; + } else { + throw new ParseException("None-hex character in unicode escape sequence: " + c); + } + } + + /** + * Returns a String where those characters that InvenioQueryParser + * expects to be escaped are escaped by a preceding \. + */ + public static String escape(String s) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // These characters are part of the query syntax and must be escaped + if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' + || c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~' + || c == '*' || c == '?' || c == '|' || c == '&') { + sb.append('\\'); + } + sb.append(c); + } + return sb.toString(); + } + + /** + * Command line tool to test InvenioQueryParser, using {@link org.apache.lucene.analysis.SimpleAnalyzer}. + * Usage:
+ * java org.apache.lucene.queryParser.InvenioQueryParser <input> + */ + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.out.println("Usage: java org.apache.lucene.queryParser.InvenioQueryParser "); + System.exit(0); + } + InvenioQueryParser qp = new InvenioQueryParser(Version.LUCENE_CURRENT, "field", + new org.apache.lucene.analysis.SimpleAnalyzer()); + Query q = qp.parse(args[0]); + System.out.println(q.toString("field")); + } +} + +PARSER_END(InvenioQueryParser) + +/* ***************** */ +/* Token Definitions */ +/* ***************** */ + +<*> TOKEN : { + <#_NUM_CHAR: ["0"-"9"] > +// every character that follows a backslash is considered as an escaped character +| <#_ESCAPED_CHAR: "\\" ~[] > +| <#_TERM_START_CHAR: ( ~[ " ", "\t", "\n", "\r", "\u3000", "+", "-", "!", "(", ")", "^", ":", + "[", "]", "\"", "{", "}", "~", "*", "?", "\\" ] + | <_ESCAPED_CHAR> ) > +| <#_TERM_CHAR: ( <_TERM_START_CHAR> | <_ESCAPED_CHAR> | "-" | "+" ) > +| <#_WHITESPACE: ( " " | "\t" | "\n" | "\r" | "\u3000") > +| <#_QUOTED_CHAR: ( ~[ "\"", "\\" ] | <_ESCAPED_CHAR> ) > +| <#_SECOND_OPERATOR: ( "refersto:" | "citedby:" | "cited:" ) > +| <#_OTHER: ( ~["/"]) > +} + + SKIP : { + < <_WHITESPACE>> +} + + TOKEN : { + +| +| +| +| +| +| +| +| +| : Boost +| )* "\""> +| )* "'"> +| (<_TERM_CHAR>)* > +| )+ ( "." (<_NUM_CHAR>)+ )? )? > +| (<_TERM_CHAR>)* "*" ) > +| | [ "*", "?" ]) (<_TERM_CHAR> | ( [ "*", "?" ] ))* > +| : RangeIn +| : RangeEx +//| > +| | <_ESCAPED_CHAR> )* ("/" | "$")) > +} + + TOKEN : { +)+ ( "." (<_NUM_CHAR>)+ )? > : DEFAULT +} + + TOKEN : { + +| : DEFAULT +| +| +} + + TOKEN : { + +| : DEFAULT +| +| +} + +// * Query ::= ( Clause )* +// * Clause ::= ["+", "-"] [ ":"] ( | "(" Query ")" ) + +int Conjunction() : { + int ret = CONJ_NONE; +} +{ + [ + { ret = CONJ_AND; } + | { ret = CONJ_OR; } + ] + { return ret; } +} + +int Modifiers() : { + int ret = MOD_NONE; +} +{ + [ + { ret = MOD_REQ; } + | { ret = MOD_NOT; } + | { ret = MOD_NOT; } + ] + { return ret; } +} + +// This makes sure that there is no garbage after the query string +Query TopLevelQuery(String field) : +{ + Query q; +} +{ + q=Query(field) + { + return q; + } +} + +Query Query(String field) : +{ + List clauses = new ArrayList(); + Query q, firstQuery=null; + int conj, mods; +} +{ + mods=Modifiers() q=Clause(field) + { + addClause(clauses, CONJ_NONE, mods, q); + if (mods == MOD_NONE) + firstQuery=q; + } + ( + conj=Conjunction() mods=Modifiers() q=Clause(field) + { addClause(clauses, conj, mods, q); } + )* + { + if (clauses.size() == 1 && firstQuery != null) + return firstQuery; + else { + return getBooleanQuery(clauses); + } + } +} + +Query Clause(String field) : { + Query q; + Token fieldToken=null, boost=null; +} +{ + [ + LOOKAHEAD(2) + ( + fieldToken= {field=discardEscapeChar(fieldToken.image);} + | {field="*";} + ) + ] + + ( + q=Term(field) + | q=Query(field) ( boost=)? + + ) + { + if (boost != null) { + float f = (float)1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + q.setBoost(f); + } catch (Exception ignored) { } + } + return q; + } +} + + +Query Term(String field) : { + Token term, boost=null, fuzzySlop=null, goop1, goop2; + boolean prefix = false; + boolean wildcard = false; + boolean fuzzy = false; + Query q; +} +{ + ( + ( + term= + | term= { wildcard=true; } + | term= { prefix=true; } + | term= { wildcard=true; } + | term= + | term= + //| term=< ( <_SECOND_OPERATOR> )+> + ) + [ fuzzySlop= { fuzzy=true; } ] + [ boost= [ fuzzySlop= { fuzzy=true; } ] ] + { + String termImage=discardEscapeChar(term.image); + if (wildcard) { + q = getWildcardQuery(field, termImage); + } else if (prefix) { + q = getPrefixQuery(field, + discardEscapeChar(term.image.substring + (0, term.image.length()-1))); + } else if (fuzzy) { + float fms = fuzzyMinSim; + try { + fms = Float.valueOf(fuzzySlop.image.substring(1)).floatValue(); + } catch (Exception ignored) { } + if(fms < 0.0f || fms > 1.0f){ + throw new ParseException("Minimum similarity for a FuzzyQuery has to be between 0.0f and 1.0f !"); + } + q = getFuzzyQuery(field, termImage,fms); + } else { + q = getFieldQuery(field, termImage); + } + } + | ( ( goop1=|goop1= ) + [ ] ( goop2=|goop2= ) + ) + [ boost= ] + { + if (goop1.kind == RANGEIN_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } + if (goop2.kind == RANGEIN_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } + q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), true); + } + | ( ( goop1=|goop1= ) + [ ] ( goop2=|goop2= ) + ) + [ boost= ] + { + if (goop1.kind == RANGEEX_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } + if (goop2.kind == RANGEEX_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } + + q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), false); + } + | term= + [ fuzzySlop= ] + [ boost= ] + { + int s = phraseSlop; + + if (fuzzySlop != null) { + try { + s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { } + } + q = getFieldQuery(field, discardEscapeChar(term.image.substring(1, term.image.length()-1)), s); + } + | term= + [ fuzzySlop= ] + [ boost= ] + { + int partialSlop = 2; + + if (fuzzySlop != null) { + try { + partialSlop = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { + partialSlop = 2; + } + } + q = getFieldQuery(field, discardEscapeChar(term.image.substring(1, term.image.length()-1)), partialSlop); + } + + ) + { + if (boost != null) { + float f = (float) 1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + } + catch (Exception ignored) { + /* Should this be handled somehow? (defaults to "no boost", if + * boost number is invalid) + */ + } + + // avoid boosting null queries, such as those caused by stop words + if (q != null) { + q.setBoost(f); + } + } + return q; + } +} diff --git a/src/java/org/apache/lucene/queryParser/InvenioQueryParserConstants.java b/src/java/org/apache/lucene/queryParser/InvenioQueryParserConstants.java new file mode 100644 index 000000000..c91e28bb1 --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/InvenioQueryParserConstants.java @@ -0,0 +1,137 @@ +/* Generated By:JavaCC: Do not edit this line. InvenioQueryParserConstants.java */ +package org.apache.lucene.queryParser; + + +/** + * Token literal values and constants. + * Generated by org.javacc.parser.OtherFilesGen#start() + */ +public interface InvenioQueryParserConstants { + + /** End of File. */ + int EOF = 0; + /** RegularExpression Id. */ + int _NUM_CHAR = 1; + /** RegularExpression Id. */ + int _ESCAPED_CHAR = 2; + /** RegularExpression Id. */ + int _TERM_START_CHAR = 3; + /** RegularExpression Id. */ + int _TERM_CHAR = 4; + /** RegularExpression Id. */ + int _WHITESPACE = 5; + /** RegularExpression Id. */ + int _QUOTED_CHAR = 6; + /** RegularExpression Id. */ + int _SECOND_OPERATOR = 7; + /** RegularExpression Id. */ + int _OTHER = 8; + /** RegularExpression Id. */ + int AND = 10; + /** RegularExpression Id. */ + int OR = 11; + /** RegularExpression Id. */ + int NOT = 12; + /** RegularExpression Id. */ + int PLUS = 13; + /** RegularExpression Id. */ + int MINUS = 14; + /** RegularExpression Id. */ + int LPAREN = 15; + /** RegularExpression Id. */ + int RPAREN = 16; + /** RegularExpression Id. */ + int COLON = 17; + /** RegularExpression Id. */ + int STAR = 18; + /** RegularExpression Id. */ + int CARAT = 19; + /** RegularExpression Id. */ + int QUOTED = 20; + /** RegularExpression Id. */ + int QUOTED_PARTIAL = 21; + /** RegularExpression Id. */ + int TERM = 22; + /** RegularExpression Id. */ + int FUZZY_SLOP = 23; + /** RegularExpression Id. */ + int PREFIXTERM = 24; + /** RegularExpression Id. */ + int WILDTERM = 25; + /** RegularExpression Id. */ + int RANGEIN_START = 26; + /** RegularExpression Id. */ + int RANGEEX_START = 27; + /** RegularExpression Id. */ + int REGEX_TERM = 28; + /** RegularExpression Id. */ + int NUMBER = 29; + /** RegularExpression Id. */ + int RANGEIN_TO = 30; + /** RegularExpression Id. */ + int RANGEIN_END = 31; + /** RegularExpression Id. */ + int RANGEIN_QUOTED = 32; + /** RegularExpression Id. */ + int RANGEIN_GOOP = 33; + /** RegularExpression Id. */ + int RANGEEX_TO = 34; + /** RegularExpression Id. */ + int RANGEEX_END = 35; + /** RegularExpression Id. */ + int RANGEEX_QUOTED = 36; + /** RegularExpression Id. */ + int RANGEEX_GOOP = 37; + + /** Lexical state. */ + int Boost = 0; + /** Lexical state. */ + int RangeEx = 1; + /** Lexical state. */ + int RangeIn = 2; + /** Lexical state. */ + int DEFAULT = 3; + + /** Literal token values. */ + String[] tokenImage = { + "", + "<_NUM_CHAR>", + "<_ESCAPED_CHAR>", + "<_TERM_START_CHAR>", + "<_TERM_CHAR>", + "<_WHITESPACE>", + "<_QUOTED_CHAR>", + "<_SECOND_OPERATOR>", + "<_OTHER>", + "", + "", + "", + "\"!\"", + "\"+\"", + "", + "\"(\"", + "\")\"", + "\":\"", + "\"*\"", + "\"^\"", + "", + "", + "", + "", + "", + "", + "\"[\"", + "\"{\"", + "", + "", + "\"TO\"", + "\"]\"", + "", + "", + "\"TO\"", + "\"}\"", + "", + "", + }; + +} diff --git a/src/java/org/apache/lucene/queryParser/InvenioQueryParserTokenManager.java b/src/java/org/apache/lucene/queryParser/InvenioQueryParserTokenManager.java new file mode 100644 index 000000000..473766562 --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/InvenioQueryParserTokenManager.java @@ -0,0 +1,1372 @@ +/* Generated By:JavaCC: Do not edit this line. InvenioQueryParserTokenManager.java */ +package org.apache.lucene.queryParser; +import java.io.IOException; +import java.io.StringReader; +import java.text.Collator; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Vector; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.CachingTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.DateField; +import org.apache.lucene.document.DateTools; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.Parameter; +import org.apache.lucene.util.Version; + +/** Token Manager. */ +public class InvenioQueryParserTokenManager implements InvenioQueryParserConstants +{ + + /** Debug output. */ + public java.io.PrintStream debugStream = System.out; + /** Set debug output. */ + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 33: + return jjStopAtPos(0, 12); + case 40: + return jjStopAtPos(0, 15); + case 41: + return jjStopAtPos(0, 16); + case 42: + return jjStartNfaWithStates_3(0, 18, 55); + case 43: + return jjStopAtPos(0, 13); + case 58: + return jjStopAtPos(0, 17); + case 91: + return jjStopAtPos(0, 26); + case 94: + return jjStopAtPos(0, 19); + case 123: + return jjStopAtPos(0, 27); + default : + return jjMoveNfa_3(0, 0); + } +} +private int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +static final long[] jjbitVec0 = { + 0x1L, 0x0L, 0x0L, 0x0L +}; +static final long[] jjbitVec1 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec3 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec4 = { + 0xfffefffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private int jjMoveNfa_3(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 55; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xfbffd4f8ffffd9ffL & l) != 0L) + { + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + } + else if ((0x100002600L & l) != 0L) + { + if (kind > 9) + kind = 9; + } + else if (curChar == 34) + jjCheckNAddStates(0, 2); + else if (curChar == 45) + { + if (kind > 14) + kind = 14; + } + if ((0x7bffd0f8ffffd9ffL & l) != 0L) + { + if (kind > 22) + kind = 22; + jjCheckNAddStates(3, 7); + } + else if (curChar == 42) + { + if (kind > 24) + kind = 24; + } + if ((0x801000000000L & l) != 0L) + jjCheckNAddStates(8, 10); + else if (curChar == 39) + jjCheckNAddStates(11, 13); + else if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 7; + break; + case 55: + case 39: + if ((0xfbfffcf8ffffd9ffL & l) == 0L) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 7: + if (curChar == 38 && kind > 10) + kind = 10; + break; + case 8: + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 7; + break; + case 16: + if (curChar == 45 && kind > 14) + kind = 14; + break; + case 23: + if (curChar == 34) + jjCheckNAddStates(0, 2); + break; + case 24: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddStates(0, 2); + break; + case 26: + jjCheckNAddStates(0, 2); + break; + case 27: + if (curChar == 34 && kind > 20) + kind = 20; + break; + case 28: + if (curChar == 39) + jjCheckNAddStates(11, 13); + break; + case 29: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddStates(11, 13); + break; + case 31: + jjCheckNAddStates(11, 13); + break; + case 32: + if (curChar == 39 && kind > 21) + kind = 21; + break; + case 34: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 23) + kind = 23; + jjAddStates(14, 15); + break; + case 35: + if (curChar == 46) + jjCheckNAdd(36); + break; + case 36: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 23) + kind = 23; + jjCheckNAdd(36); + break; + case 37: + if (curChar == 42 && kind > 24) + kind = 24; + break; + case 38: + if ((0xfbffd4f8ffffd9ffL & l) == 0L) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 41: + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 42: + if ((0x801000000000L & l) != 0L) + jjCheckNAddStates(8, 10); + break; + case 43: + if ((0xffff7fffffffffffL & l) != 0L) + jjCheckNAddStates(8, 10); + break; + case 45: + jjCheckNAddStates(8, 10); + break; + case 46: + if ((0x801000000000L & l) != 0L && kind > 28) + kind = 28; + break; + case 47: + if ((0x7bffd0f8ffffd9ffL & l) == 0L) + break; + if (kind > 22) + kind = 22; + jjCheckNAddStates(3, 7); + break; + case 48: + if ((0x7bfff8f8ffffd9ffL & l) == 0L) + break; + if (kind > 22) + kind = 22; + jjCheckNAddTwoStates(48, 49); + break; + case 50: + if (kind > 22) + kind = 22; + jjCheckNAddTwoStates(48, 49); + break; + case 51: + if ((0x7bfff8f8ffffd9ffL & l) != 0L) + jjCheckNAddStates(16, 18); + break; + case 53: + jjCheckNAddStates(16, 18); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x97ffffff87ffffffL & l) != 0L) + { + if (kind > 22) + kind = 22; + jjCheckNAddStates(3, 7); + } + else if (curChar == 92) + jjCheckNAddStates(19, 21); + else if (curChar == 126) + { + if (kind > 23) + kind = 23; + jjstateSet[jjnewStateCnt++] = 34; + } + if ((0x97ffffff87ffffffL & l) != 0L) + { + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + } + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 21; + else if (curChar == 78) + jjstateSet[jjnewStateCnt++] = 18; + else if (curChar == 124) + { + if (kind > 11) + kind = 11; + } + else if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 13; + else if (curChar == 79) + jjstateSet[jjnewStateCnt++] = 9; + else if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 5; + else if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 2; + if (curChar == 124) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 55: + if ((0x97ffffff87ffffffL & l) != 0L) + { + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + } + else if (curChar == 92) + jjCheckNAddTwoStates(41, 41); + break; + case 1: + if (curChar == 68 && kind > 10) + kind = 10; + break; + case 2: + if (curChar == 78) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 3: + if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 2; + break; + case 4: + if (curChar == 100 && kind > 10) + kind = 10; + break; + case 5: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 6: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 5; + break; + case 9: + if (curChar == 82 && kind > 11) + kind = 11; + break; + case 10: + if (curChar == 79) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 124 && kind > 11) + kind = 11; + break; + case 12: + if (curChar == 124) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 114 && kind > 11) + kind = 11; + break; + case 14: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 13; + break; + case 15: + if (curChar == 124 && kind > 11) + kind = 11; + break; + case 17: + if (curChar == 84 && kind > 14) + kind = 14; + break; + case 18: + if (curChar == 79) + jjstateSet[jjnewStateCnt++] = 17; + break; + case 19: + if (curChar == 78) + jjstateSet[jjnewStateCnt++] = 18; + break; + case 20: + if (curChar == 116 && kind > 14) + kind = 14; + break; + case 21: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 20; + break; + case 22: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 21; + break; + case 24: + if ((0xffffffffefffffffL & l) != 0L) + jjCheckNAddStates(0, 2); + break; + case 25: + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 26; + break; + case 26: + jjCheckNAddStates(0, 2); + break; + case 29: + if ((0xffffffffefffffffL & l) != 0L) + jjCheckNAddStates(11, 13); + break; + case 30: + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 31; + break; + case 31: + jjCheckNAddStates(11, 13); + break; + case 33: + if (curChar != 126) + break; + if (kind > 23) + kind = 23; + jjstateSet[jjnewStateCnt++] = 34; + break; + case 38: + if ((0x97ffffff87ffffffL & l) == 0L) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 39: + if ((0x97ffffff87ffffffL & l) == 0L) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 40: + if (curChar == 92) + jjCheckNAddTwoStates(41, 41); + break; + case 41: + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 43: + case 45: + jjCheckNAddStates(8, 10); + break; + case 44: + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 45; + break; + case 47: + if ((0x97ffffff87ffffffL & l) == 0L) + break; + if (kind > 22) + kind = 22; + jjCheckNAddStates(3, 7); + break; + case 48: + if ((0x97ffffff87ffffffL & l) == 0L) + break; + if (kind > 22) + kind = 22; + jjCheckNAddTwoStates(48, 49); + break; + case 49: + if (curChar == 92) + jjCheckNAddTwoStates(50, 50); + break; + case 50: + if (kind > 22) + kind = 22; + jjCheckNAddTwoStates(48, 49); + break; + case 51: + if ((0x97ffffff87ffffffL & l) != 0L) + jjCheckNAddStates(16, 18); + break; + case 52: + if (curChar == 92) + jjCheckNAddTwoStates(53, 53); + break; + case 53: + jjCheckNAddStates(16, 18); + break; + case 54: + if (curChar == 92) + jjCheckNAddStates(19, 21); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { + if (kind > 9) + kind = 9; + } + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + { + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + } + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + { + if (kind > 22) + kind = 22; + jjCheckNAddStates(3, 7); + } + break; + case 55: + case 39: + if (!jjCanMove_2(hiByte, i1, i2, l1, l2)) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 24: + case 26: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(0, 2); + break; + case 29: + case 31: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(11, 13); + break; + case 38: + if (!jjCanMove_2(hiByte, i1, i2, l1, l2)) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 41: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 25) + kind = 25; + jjCheckNAddTwoStates(39, 40); + break; + case 43: + case 45: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(8, 10); + break; + case 47: + if (!jjCanMove_2(hiByte, i1, i2, l1, l2)) + break; + if (kind > 22) + kind = 22; + jjCheckNAddStates(3, 7); + break; + case 48: + if (!jjCanMove_2(hiByte, i1, i2, l1, l2)) + break; + if (kind > 22) + kind = 22; + jjCheckNAddTwoStates(48, 49); + break; + case 50: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 22) + kind = 22; + jjCheckNAddTwoStates(48, 49); + break; + case 51: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(16, 18); + break; + case 53: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(16, 18); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 55 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x400000000L) != 0L) + { + jjmatchedKind = 37; + return 6; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 84: + return jjMoveStringLiteralDfa1_1(0x400000000L); + case 125: + return jjStopAtPos(0, 35); + default : + return jjMoveNfa_1(0, 0); + } +} +private int jjMoveStringLiteralDfa1_1(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(0, active0); + return 1; + } + switch(curChar) + { + case 79: + if ((active0 & 0x400000000L) != 0L) + return jjStartNfaWithStates_1(1, 34, 6); + break; + default : + break; + } + return jjStartNfa_1(0, active0); +} +private int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private int jjMoveNfa_1(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 7; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xfffffffeffffffffL & l) != 0L) + { + if (kind > 37) + kind = 37; + jjCheckNAdd(6); + } + if ((0x100002600L & l) != 0L) + { + if (kind > 9) + kind = 9; + } + else if (curChar == 34) + jjCheckNAddTwoStates(2, 4); + break; + case 1: + if (curChar == 34) + jjCheckNAddTwoStates(2, 4); + break; + case 2: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddStates(22, 24); + break; + case 3: + if (curChar == 34) + jjCheckNAddStates(22, 24); + break; + case 5: + if (curChar == 34 && kind > 36) + kind = 36; + break; + case 6: + if ((0xfffffffeffffffffL & l) == 0L) + break; + if (kind > 37) + kind = 37; + jjCheckNAdd(6); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + case 6: + if ((0xdfffffffffffffffL & l) == 0L) + break; + if (kind > 37) + kind = 37; + jjCheckNAdd(6); + break; + case 2: + jjAddStates(22, 24); + break; + case 4: + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 3; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { + if (kind > 9) + kind = 9; + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 37) + kind = 37; + jjCheckNAdd(6); + } + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjAddStates(22, 24); + break; + case 6: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 37) + kind = 37; + jjCheckNAdd(6); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 7 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private int jjMoveStringLiteralDfa0_0() +{ + return jjMoveNfa_0(0, 0); +} +private int jjMoveNfa_0(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 29) + kind = 29; + jjAddStates(25, 26); + break; + case 1: + if (curChar == 46) + jjCheckNAdd(2); + break; + case 2: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 29) + kind = 29; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x40000000L) != 0L) + { + jjmatchedKind = 33; + return 6; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 84: + return jjMoveStringLiteralDfa1_2(0x40000000L); + case 93: + return jjStopAtPos(0, 31); + default : + return jjMoveNfa_2(0, 0); + } +} +private int jjMoveStringLiteralDfa1_2(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(0, active0); + return 1; + } + switch(curChar) + { + case 79: + if ((active0 & 0x40000000L) != 0L) + return jjStartNfaWithStates_2(1, 30, 6); + break; + default : + break; + } + return jjStartNfa_2(0, active0); +} +private int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private int jjMoveNfa_2(int startState, int curPos) +{ + int startsAt = 0; + jjnewStateCnt = 7; + int i = 1; + jjstateSet[0] = startState; + int kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xfffffffeffffffffL & l) != 0L) + { + if (kind > 33) + kind = 33; + jjCheckNAdd(6); + } + if ((0x100002600L & l) != 0L) + { + if (kind > 9) + kind = 9; + } + else if (curChar == 34) + jjCheckNAddTwoStates(2, 4); + break; + case 1: + if (curChar == 34) + jjCheckNAddTwoStates(2, 4); + break; + case 2: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddStates(22, 24); + break; + case 3: + if (curChar == 34) + jjCheckNAddStates(22, 24); + break; + case 5: + if (curChar == 34 && kind > 32) + kind = 32; + break; + case 6: + if ((0xfffffffeffffffffL & l) == 0L) + break; + if (kind > 33) + kind = 33; + jjCheckNAdd(6); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + case 6: + if ((0xffffffffdfffffffL & l) == 0L) + break; + if (kind > 33) + kind = 33; + jjCheckNAdd(6); + break; + case 2: + jjAddStates(22, 24); + break; + case 4: + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 3; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + { + if (kind > 9) + kind = 9; + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 33) + kind = 33; + jjCheckNAdd(6); + } + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjAddStates(22, 24); + break; + case 6: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 33) + kind = 33; + jjCheckNAdd(6); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 7 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { + 24, 25, 27, 48, 51, 37, 52, 49, 43, 44, 46, 29, 30, 32, 34, 35, + 51, 37, 52, 50, 53, 41, 2, 4, 5, 0, 1, +}; +private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 48: + return ((jjbitVec0[i2] & l2) != 0L); + default : + return false; + } +} +private static final boolean jjCanMove_1(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec3[i2] & l2) != 0L); + default : + if ((jjbitVec1[i1] & l1) != 0L) + return true; + return false; + } +} +private static final boolean jjCanMove_2(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec3[i2] & l2) != 0L); + case 48: + return ((jjbitVec1[i2] & l2) != 0L); + default : + if ((jjbitVec4[i1] & l1) != 0L) + return true; + return false; + } +} + +/** Token literal values. */ +public static final String[] jjstrLiteralImages = { +"", null, null, null, null, null, null, null, null, null, null, null, "\41", +"\53", null, "\50", "\51", "\72", "\52", "\136", null, null, null, null, null, null, +"\133", "\173", null, null, "\124\117", "\135", null, null, "\124\117", "\175", null, +null, }; + +/** Lexer state names. */ +public static final String[] lexStateNames = { + "Boost", + "RangeEx", + "RangeIn", + "DEFAULT", +}; + +/** Lex State array. */ +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, + -1, 2, 1, -1, 3, -1, 3, -1, -1, -1, 3, -1, -1, +}; +static final long[] jjtoToken = { + 0x3ffffffc01L, +}; +static final long[] jjtoSkip = { + 0x200L, +}; +protected CharStream input_stream; +private final int[] jjrounds = new int[55]; +private final int[] jjstateSet = new int[110]; +protected char curChar; +/** Constructor. */ +public InvenioQueryParserTokenManager(CharStream stream){ + input_stream = stream; +} + +/** Constructor. */ +public InvenioQueryParserTokenManager(CharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} + +/** Reinitialise parser. */ +public void ReInit(CharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 55; i-- > 0;) + jjrounds[i] = 0x80000000; +} + +/** Reinitialise parser. */ +public void ReInit(CharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} + +/** Switch to specified lex state. */ +public void SwitchTo(int lexState) +{ + if (lexState >= 4 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + final Token t; + final String curTokenImage; + final int beginLine; + final int endLine; + final int beginColumn; + final int endColumn; + String im = jjstrLiteralImages[jjmatchedKind]; + curTokenImage = (im == null) ? input_stream.GetImage() : im; + beginLine = input_stream.getBeginLine(); + beginColumn = input_stream.getBeginColumn(); + endLine = input_stream.getEndLine(); + endColumn = input_stream.getEndColumn(); + t = Token.newToken(jjmatchedKind, curTokenImage); + + t.beginLine = beginLine; + t.endLine = endLine; + t.beginColumn = beginColumn; + t.endColumn = endColumn; + + return t; +} + +int curLexState = 3; +int defaultLexState = 3; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +/** Get the next Token. */ +public Token getNextToken() +{ + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + return matchedToken; + } + + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else + { + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } +} + +private void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} + +private void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} + +} diff --git a/src/java/org/apache/lucene/queryParser/ParseException.java b/src/java/org/apache/lucene/queryParser/ParseException.java new file mode 100644 index 000000000..276fb74aa --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/ParseException.java @@ -0,0 +1,187 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 5.0 */ +/* JavaCCOptions:KEEP_LINE_COL=null */ +package org.apache.lucene.queryParser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(initialise(currentTokenVal, expectedTokenSequencesVal, tokenImageVal)); + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + } + + /** Constructor with message. */ + public ParseException(String message) { + super(message); + } + + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * It uses "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser) the correct error message + * gets displayed. + */ + private static String initialise(Token currentToken, + int[][] expectedTokenSequences, + String[] tokenImage) { + String eol = System.getProperty("line.separator", "\n"); + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(' '); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += " " + tokenImage[tok.kind]; + retval += " \""; + retval += add_escapes(tok.image); + retval += " \""; + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + static String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} +/* JavaCC - OriginalChecksum=2e7670d6260cd2ac6c9cbda0075541b7 (do not edit this line) */ diff --git a/src/java/org/apache/lucene/queryParser/QueryParser.jj b/src/java/org/apache/lucene/queryParser/QueryParser.jj new file mode 100755 index 000000000..c739104f7 --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/QueryParser.jj @@ -0,0 +1,1483 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +options { + STATIC=false; + JAVA_UNICODE_ESCAPE=true; + USER_CHAR_STREAM=true; +} + +PARSER_BEGIN(QueryParser) + +package org.apache.lucene.queryParser; + +import java.io.IOException; +import java.io.StringReader; +import java.text.Collator; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Vector; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.CachingTokenFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.DateField; +import org.apache.lucene.document.DateTools; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.Parameter; +import org.apache.lucene.util.Version; + +/** + * This class is generated by JavaCC. The most important method is + * {@link #parse(String)}. + * + * The syntax for query strings is as follows: + * A Query is a series of clauses. + * A clause may be prefixed by: + *

    + *
  • a plus (+) or a minus (-) sign, indicating + * that the clause is required or prohibited respectively; or + *
  • a term followed by a colon, indicating the field to be searched. + * This enables one to construct queries which search multiple fields. + *
+ * + * A clause may be either: + *
    + *
  • a term, indicating all the documents that contain this term; or + *
  • a nested query, enclosed in parentheses. Note that this may be used + * with a +/- prefix to require any of a set of + * terms. + *
+ * + * Thus, in BNF, the query grammar is: + *
+ *   Query  ::= ( Clause )*
+ *   Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")" )
+ * 
+ * + *

+ * Examples of appropriately formatted queries can be found in the query syntax + * documentation. + *

+ * + *

+ * In {@link TermRangeQuery}s, QueryParser tries to detect date values, e.g. + * date:[6/1/2005 TO 6/4/2005] produces a range query that searches + * for "date" fields between 2005-06-01 and 2005-06-04. Note that the format + * of the accepted input depends on {@link #setLocale(Locale) the locale}. + * By default a date is converted into a search term using the deprecated + * {@link DateField} for compatibility reasons. + * To use the new {@link DateTools} to convert dates, a + * {@link org.apache.lucene.document.DateTools.Resolution} has to be set. + *

+ *

+ * The date resolution that shall be used for RangeQueries can be set + * using {@link #setDateResolution(DateTools.Resolution)} + * or {@link #setDateResolution(String, DateTools.Resolution)}. The former + * sets the default date resolution for all fields, whereas the latter can + * be used to set field specific date resolutions. Field specific date + * resolutions take, if set, precedence over the default date resolution. + *

+ *

+ * If you use neither {@link DateField} nor {@link DateTools} in your + * index, you can create your own + * query parser that inherits QueryParser and overwrites + * {@link #getRangeQuery(String, String, String, boolean)} to + * use a different method for date conversion. + *

+ * + *

Note that QueryParser is not thread-safe.

+ * + *

NOTE: there is a new QueryParser in contrib, which matches + * the same syntax as this class, but is more modular, + * enabling substantial customization to how a query is created. + * + * + *

NOTE: You must specify the required {@link Version} + * compatibility when creating QueryParser: + *

+ */ +public class QueryParser { + + private static final int CONJ_NONE = 0; + private static final int CONJ_AND = 1; + private static final int CONJ_OR = 2; + + private static final int MOD_NONE = 0; + private static final int MOD_NOT = 10; + private static final int MOD_REQ = 11; + + // make it possible to call setDefaultOperator() without accessing + // the nested class: + /** Alternative form of QueryParser.Operator.AND */ + public static final Operator AND_OPERATOR = Operator.AND; + /** Alternative form of QueryParser.Operator.OR */ + public static final Operator OR_OPERATOR = Operator.OR; + + /** The actual operator that parser uses to combine query terms */ + private Operator operator = OR_OPERATOR; + + boolean lowercaseExpandedTerms = true; + MultiTermQuery.RewriteMethod multiTermRewriteMethod = MultiTermQuery.CONSTANT_SCORE_AUTO_REWRITE_DEFAULT; + boolean allowLeadingWildcard = false; + boolean enablePositionIncrements = true; + + Analyzer analyzer; + String field; + int phraseSlop = 0; + float fuzzyMinSim = FuzzyQuery.defaultMinSimilarity; + int fuzzyPrefixLength = FuzzyQuery.defaultPrefixLength; + Locale locale = Locale.getDefault(); + + // the default date resolution + DateTools.Resolution dateResolution = null; + // maps field names to date resolutions + Map fieldToDateResolution = null; + + // The collator to use when determining range inclusion, + // for use when constructing RangeQuerys. + Collator rangeCollator = null; + + /** The default operator for parsing queries. + * Use {@link QueryParser#setDefaultOperator} to change it. + */ + static public final class Operator extends Parameter { + private Operator(String name) { + super(name); + } + static public final Operator OR = new Operator("OR"); + static public final Operator AND = new Operator("AND"); + } + + + /** Constructs a query parser. + * @param f the default field for query terms. + * @param a used to find terms in the query text. + * @deprecated Use {@link #QueryParser(Version, String, Analyzer)} instead + */ + public QueryParser(String f, Analyzer a) { + this(Version.LUCENE_24, f, a); + } + + /** Constructs a query parser. + * @param matchVersion Lucene version to match. See {@link above) + * @param f the default field for query terms. + * @param a used to find terms in the query text. + */ + public QueryParser(Version matchVersion, String f, Analyzer a) { + this(new FastCharStream(new StringReader(""))); + analyzer = a; + field = f; + if (matchVersion.onOrAfter(Version.LUCENE_29)) { + enablePositionIncrements = true; + } else { + enablePositionIncrements = false; + } + } + + /** Parses a query string, returning a {@link org.apache.lucene.search.Query}. + * @param query the query string to be parsed. + * @throws ParseException if the parsing fails + */ + public Query parse(String query) throws ParseException { + ReInit(new FastCharStream(new StringReader(query))); + try { + // TopLevelQuery is a Query followed by the end-of-input (EOF) + Query res = TopLevelQuery(field); + return res!=null ? res : newBooleanQuery(false); + } + catch (ParseException tme) { + // rethrow to include the original query: + ParseException e = new ParseException("Cannot parse '" +query+ "': " + tme.getMessage()); + e.initCause(tme); + throw e; + } + catch (TokenMgrError tme) { + ParseException e = new ParseException("Cannot parse '" +query+ "': " + tme.getMessage()); + e.initCause(tme); + throw e; + } + catch (BooleanQuery.TooManyClauses tmc) { + ParseException e = new ParseException("Cannot parse '" +query+ "': too many boolean clauses"); + e.initCause(tmc); + throw e; + } + } + + /** + * @return Returns the analyzer. + */ + public Analyzer getAnalyzer() { + return analyzer; + } + + /** + * @return Returns the field. + */ + public String getField() { + return field; + } + + /** + * Get the minimal similarity for fuzzy queries. + */ + public float getFuzzyMinSim() { + return fuzzyMinSim; + } + + /** + * Set the minimum similarity for fuzzy queries. + * Default is 0.5f. + */ + public void setFuzzyMinSim(float fuzzyMinSim) { + this.fuzzyMinSim = fuzzyMinSim; + } + + /** + * Get the prefix length for fuzzy queries. + * @return Returns the fuzzyPrefixLength. + */ + public int getFuzzyPrefixLength() { + return fuzzyPrefixLength; + } + + /** + * Set the prefix length for fuzzy queries. Default is 0. + * @param fuzzyPrefixLength The fuzzyPrefixLength to set. + */ + public void setFuzzyPrefixLength(int fuzzyPrefixLength) { + this.fuzzyPrefixLength = fuzzyPrefixLength; + } + + /** + * Sets the default slop for phrases. If zero, then exact phrase matches + * are required. Default value is zero. + */ + public void setPhraseSlop(int phraseSlop) { + this.phraseSlop = phraseSlop; + } + + /** + * Gets the default slop for phrases. + */ + public int getPhraseSlop() { + return phraseSlop; + } + + + /** + * Set to true to allow leading wildcard characters. + *

+ * When set, * or ? are allowed as + * the first character of a PrefixQuery and WildcardQuery. + * Note that this can produce very slow + * queries on big indexes. + *

+ * Default: false. + */ + public void setAllowLeadingWildcard(boolean allowLeadingWildcard) { + this.allowLeadingWildcard = allowLeadingWildcard; + } + + /** + * @see #setAllowLeadingWildcard(boolean) + */ + public boolean getAllowLeadingWildcard() { + return allowLeadingWildcard; + } + + /** + * Set to true to enable position increments in result query. + *

+ * When set, result phrase and multi-phrase queries will + * be aware of position increments. + * Useful when e.g. a StopFilter increases the position increment of + * the token that follows an omitted token. + *

+ * Default: false. + */ + public void setEnablePositionIncrements(boolean enable) { + this.enablePositionIncrements = enable; + } + + /** + * @see #setEnablePositionIncrements(boolean) + */ + public boolean getEnablePositionIncrements() { + return enablePositionIncrements; + } + + /** + * Sets the boolean operator of the QueryParser. + * In default mode (OR_OPERATOR) terms without any modifiers + * are considered optional: for example capital of Hungary is equal to + * capital OR of OR Hungary.
+ * In AND_OPERATOR mode terms are considered to be in conjunction: the + * above mentioned query is parsed as capital AND of AND Hungary + */ + public void setDefaultOperator(Operator op) { + this.operator = op; + } + + + /** + * Gets implicit operator setting, which will be either AND_OPERATOR + * or OR_OPERATOR. + */ + public Operator getDefaultOperator() { + return operator; + } + + + /** + * Whether terms of wildcard, prefix, fuzzy and range queries are to be automatically + * lower-cased or not. Default is true. + */ + public void setLowercaseExpandedTerms(boolean lowercaseExpandedTerms) { + this.lowercaseExpandedTerms = lowercaseExpandedTerms; + } + + + /** + * @see #setLowercaseExpandedTerms(boolean) + */ + public boolean getLowercaseExpandedTerms() { + return lowercaseExpandedTerms; + } + + /** + * @deprecated Please use {@link #setMultiTermRewriteMethod} instead. + */ + public void setUseOldRangeQuery(boolean useOldRangeQuery) { + if (useOldRangeQuery) { + setMultiTermRewriteMethod(MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE); + } else { + setMultiTermRewriteMethod(MultiTermQuery.CONSTANT_SCORE_AUTO_REWRITE_DEFAULT); + } + } + + + /** + * @deprecated Please use {@link #getMultiTermRewriteMethod} instead. + */ + public boolean getUseOldRangeQuery() { + if (getMultiTermRewriteMethod() == MultiTermQuery.SCORING_BOOLEAN_QUERY_REWRITE) { + return true; + } else { + return false; + } + } + + /** + * By default QueryParser uses {@link MultiTermQuery#CONSTANT_SCORE_AUTO_REWRITE_DEFAULT} + * when creating a PrefixQuery, WildcardQuery or RangeQuery. This implementation is generally preferable because it + * a) Runs faster b) Does not have the scarcity of terms unduly influence score + * c) avoids any "TooManyBooleanClauses" exception. + * However, if your application really needs to use the + * old-fashioned BooleanQuery expansion rewriting and the above + * points are not relevant then use this to change + * the rewrite method. + */ + public void setMultiTermRewriteMethod(MultiTermQuery.RewriteMethod method) { + multiTermRewriteMethod = method; + } + + + /** + * @see #setMultiTermRewriteMethod + */ + public MultiTermQuery.RewriteMethod getMultiTermRewriteMethod() { + return multiTermRewriteMethod; + } + + /** + * Set locale used by date range parsing. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /** + * Returns current locale, allowing access by subclasses. + */ + public Locale getLocale() { + return locale; + } + + /** + * Sets the default date resolution used by RangeQueries for fields for which no + * specific date resolutions has been set. Field specific resolutions can be set + * with {@link #setDateResolution(String, DateTools.Resolution)}. + * + * @param dateResolution the default date resolution to set + */ + public void setDateResolution(DateTools.Resolution dateResolution) { + this.dateResolution = dateResolution; + } + + /** + * Sets the date resolution used by RangeQueries for a specific field. + * + * @param fieldName field for which the date resolution is to be set + * @param dateResolution date resolution to set + */ + public void setDateResolution(String fieldName, DateTools.Resolution dateResolution) { + if (fieldName == null) { + throw new IllegalArgumentException("Field cannot be null."); + } + + if (fieldToDateResolution == null) { + // lazily initialize HashMap + fieldToDateResolution = new HashMap(); + } + + fieldToDateResolution.put(fieldName, dateResolution); + } + + /** + * Returns the date resolution that is used by RangeQueries for the given field. + * Returns null, if no default or field specific date resolution has been set + * for the given field. + * + */ + public DateTools.Resolution getDateResolution(String fieldName) { + if (fieldName == null) { + throw new IllegalArgumentException("Field cannot be null."); + } + + if (fieldToDateResolution == null) { + // no field specific date resolutions set; return default date resolution instead + return this.dateResolution; + } + + DateTools.Resolution resolution = (DateTools.Resolution) fieldToDateResolution.get(fieldName); + if (resolution == null) { + // no date resolutions set for the given field; return default date resolution instead + resolution = this.dateResolution; + } + + return resolution; + } + + /** + * Sets the collator used to determine index term inclusion in ranges + * for RangeQuerys. + *

+ * WARNING: Setting the rangeCollator to a non-null + * collator using this method will cause every single index Term in the + * Field referenced by lowerTerm and/or upperTerm to be examined. + * Depending on the number of index Terms in this Field, the operation could + * be very slow. + * + * @param rc the collator to use when constructing RangeQuerys + */ + public void setRangeCollator(Collator rc) { + rangeCollator = rc; + } + + /** + * @return the collator used to determine index term inclusion in ranges + * for RangeQuerys. + */ + public Collator getRangeCollator() { + return rangeCollator; + } + + /** + * @deprecated use {@link #addClause(List, int, int, Query)} instead. + */ + protected void addClause(Vector clauses, int conj, int mods, Query q) { + addClause((List) clauses, conj, mods, q); + } + + protected void addClause(List clauses, int conj, int mods, Query q) { + boolean required, prohibited; + + // If this term is introduced by AND, make the preceding term required, + // unless it's already prohibited + if (clauses.size() > 0 && conj == CONJ_AND) { + BooleanClause c = (BooleanClause) clauses.get(clauses.size()-1); + if (!c.isProhibited()) + c.setOccur(BooleanClause.Occur.MUST); + } + + if (clauses.size() > 0 && operator == AND_OPERATOR && conj == CONJ_OR) { + // If this term is introduced by OR, make the preceding term optional, + // unless it's prohibited (that means we leave -a OR b but +a OR b-->a OR b) + // notice if the input is a OR b, first term is parsed as required; without + // this modification a OR b would parsed as +a OR b + BooleanClause c = (BooleanClause) clauses.get(clauses.size()-1); + if (!c.isProhibited()) + c.setOccur(BooleanClause.Occur.SHOULD); + } + + // We might have been passed a null query; the term might have been + // filtered away by the analyzer. + if (q == null) + return; + + if (operator == OR_OPERATOR) { + // We set REQUIRED if we're introduced by AND or +; PROHIBITED if + // introduced by NOT or -; make sure not to set both. + prohibited = (mods == MOD_NOT); + required = (mods == MOD_REQ); + if (conj == CONJ_AND && !prohibited) { + required = true; + } + } else { + // We set PROHIBITED if we're introduced by NOT or -; We set REQUIRED + // if not PROHIBITED and not introduced by OR + prohibited = (mods == MOD_NOT); + required = (!prohibited && conj != CONJ_OR); + } + if (required && !prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST)); + else if (!required && !prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.SHOULD)); + else if (!required && prohibited) + clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST_NOT)); + else + throw new RuntimeException("Clause cannot be both required and prohibited"); + } + + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText) throws ParseException { + // Use the analyzer to get all the tokens, and then build a TermQuery, + // PhraseQuery, or nothing based on the term count + + TokenStream source; + try { + source = analyzer.reusableTokenStream(field, new StringReader(queryText)); + source.reset(); + } catch (IOException e) { + source = analyzer.tokenStream(field, new StringReader(queryText)); + } + CachingTokenFilter buffer = new CachingTokenFilter(source); + TermAttribute termAtt = null; + PositionIncrementAttribute posIncrAtt = null; + int numTokens = 0; + + boolean success = false; + try { + buffer.reset(); + success = true; + } catch (IOException e) { + // success==false if we hit an exception + } + if (success) { + if (buffer.hasAttribute(TermAttribute.class)) { + termAtt = (TermAttribute) buffer.getAttribute(TermAttribute.class); + } + if (buffer.hasAttribute(PositionIncrementAttribute.class)) { + posIncrAtt = (PositionIncrementAttribute) buffer.getAttribute(PositionIncrementAttribute.class); + } + } + + int positionCount = 0; + boolean severalTokensAtSamePosition = false; + + boolean hasMoreTokens = false; + if (termAtt != null) { + try { + hasMoreTokens = buffer.incrementToken(); + while (hasMoreTokens) { + numTokens++; + int positionIncrement = (posIncrAtt != null) ? posIncrAtt.getPositionIncrement() : 1; + if (positionIncrement != 0) { + positionCount += positionIncrement; + } else { + severalTokensAtSamePosition = true; + } + hasMoreTokens = buffer.incrementToken(); + } + } catch (IOException e) { + // ignore + } + } + try { + // rewind the buffer stream + buffer.reset(); + + // close original stream - all tokens buffered + source.close(); + } + catch (IOException e) { + // ignore + } + + if (numTokens == 0) + return null; + else if (numTokens == 1) { + String term = null; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + return newTermQuery(new Term(field, term)); + } else { + if (severalTokensAtSamePosition) { + if (positionCount == 1) { + // no phrase query: + BooleanQuery q = newBooleanQuery(true); + for (int i = 0; i < numTokens; i++) { + String term = null; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + Query currentQuery = newTermQuery( + new Term(field, term)); + q.add(currentQuery, BooleanClause.Occur.SHOULD); + } + return q; + } + else { + // phrase query: + MultiPhraseQuery mpq = newMultiPhraseQuery(); + mpq.setSlop(phraseSlop); + List multiTerms = new ArrayList(); + int position = -1; + for (int i = 0; i < numTokens; i++) { + String term = null; + int positionIncrement = 1; + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + if (posIncrAtt != null) { + positionIncrement = posIncrAtt.getPositionIncrement(); + } + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + if (positionIncrement > 0 && multiTerms.size() > 0) { + if (enablePositionIncrements) { + mpq.add((Term[])multiTerms.toArray(new Term[0]),position); + } else { + mpq.add((Term[])multiTerms.toArray(new Term[0])); + } + multiTerms.clear(); + } + position += positionIncrement; + multiTerms.add(new Term(field, term)); + } + if (enablePositionIncrements) { + mpq.add((Term[])multiTerms.toArray(new Term[0]),position); + } else { + mpq.add((Term[])multiTerms.toArray(new Term[0])); + } + return mpq; + } + } + else { + PhraseQuery pq = newPhraseQuery(); + pq.setSlop(phraseSlop); + int position = -1; + + + for (int i = 0; i < numTokens; i++) { + String term = null; + int positionIncrement = 1; + + try { + boolean hasNext = buffer.incrementToken(); + assert hasNext == true; + term = termAtt.term(); + if (posIncrAtt != null) { + positionIncrement = posIncrAtt.getPositionIncrement(); + } + } catch (IOException e) { + // safe to ignore, because we know the number of tokens + } + + if (enablePositionIncrements) { + position += positionIncrement; + pq.add(new Term(field, term),position); + } else { + pq.add(new Term(field, term)); + } + } + return pq; + } + } + } + + + + /** + * Base implementation delegates to {@link #getFieldQuery(String,String)}. + * This method may be overridden, for example, to return + * a SpanNearQuery instead of a PhraseQuery. + * + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFieldQuery(String field, String queryText, int slop) + throws ParseException { + Query query = getFieldQuery(field, queryText); + + if (query instanceof PhraseQuery) { + ((PhraseQuery) query).setSlop(slop); + } + if (query instanceof MultiPhraseQuery) { + ((MultiPhraseQuery) query).setSlop(slop); + } + + return query; + } + + + /** + * @exception ParseException throw in overridden method to disallow + */ + protected Query getRangeQuery(String field, + String part1, + String part2, + boolean inclusive) throws ParseException + { + if (lowercaseExpandedTerms) { + part1 = part1.toLowerCase(); + part2 = part2.toLowerCase(); + } + try { + DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale); + df.setLenient(true); + Date d1 = df.parse(part1); + Date d2 = df.parse(part2); + if (inclusive) { + // The user can only specify the date, not the time, so make sure + // the time is set to the latest possible time of that date to really + // include all documents: + Calendar cal = Calendar.getInstance(locale); + cal.setTime(d2); + cal.set(Calendar.HOUR_OF_DAY, 23); + cal.set(Calendar.MINUTE, 59); + cal.set(Calendar.SECOND, 59); + cal.set(Calendar.MILLISECOND, 999); + d2 = cal.getTime(); + } + DateTools.Resolution resolution = getDateResolution(field); + if (resolution == null) { + // no default or field specific date resolution has been set, + // use deprecated DateField to maintain compatibility with + // pre-1.9 Lucene versions. + part1 = DateField.dateToString(d1); + part2 = DateField.dateToString(d2); + } else { + part1 = DateTools.dateToString(d1, resolution); + part2 = DateTools.dateToString(d2, resolution); + } + } + catch (Exception e) { } + + return newRangeQuery(field, part1, part2, inclusive); + } + + /** + * Builds a new BooleanQuery instance + * @param disableCoord disable coord + * @return new BooleanQuery instance + */ + protected BooleanQuery newBooleanQuery(boolean disableCoord) { + return new BooleanQuery(disableCoord); + } + + /** + * Builds a new BooleanClause instance + * @param q sub query + * @param occur how this clause should occur when matching documents + * @return new BooleanClause instance + */ + protected BooleanClause newBooleanClause(Query q, BooleanClause.Occur occur) { + return new BooleanClause(q, occur); + } + + /** + * Builds a new TermQuery instance + * @param term term + * @return new TermQuery instance + */ + protected Query newTermQuery(Term term){ + return new TermQuery(term); + } + + /** + * Builds a new PhraseQuery instance + * @return new PhraseQuery instance + */ + protected PhraseQuery newPhraseQuery(){ + return new PhraseQuery(); + } + + /** + * Builds a new MultiPhraseQuery instance + * @return new MultiPhraseQuery instance + */ + protected MultiPhraseQuery newMultiPhraseQuery(){ + return new MultiPhraseQuery(); + } + + /** + * Builds a new PrefixQuery instance + * @param prefix Prefix term + * @return new PrefixQuery instance + */ + protected Query newPrefixQuery(Term prefix){ + PrefixQuery query = new PrefixQuery(prefix); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Builds a new FuzzyQuery instance + * @param term Term + * @param minimumSimilarity minimum similarity + * @param prefixLength prefix length + * @return new FuzzyQuery Instance + */ + protected Query newFuzzyQuery(Term term, float minimumSimilarity, int prefixLength) { + // FuzzyQuery doesn't yet allow constant score rewrite + return new FuzzyQuery(term,minimumSimilarity,prefixLength); + } + + /** + * Builds a new TermRangeQuery instance + * @param field Field + * @param part1 min + * @param part2 max + * @param inclusive true if range is inclusive + * @return new TermRangeQuery instance + */ + protected Query newRangeQuery(String field, String part1, String part2, boolean inclusive) { + final TermRangeQuery query = new TermRangeQuery(field, part1, part2, inclusive, inclusive, rangeCollator); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Builds a new MatchAllDocsQuery instance + * @return new MatchAllDocsQuery instance + */ + protected Query newMatchAllDocsQuery() { + return new MatchAllDocsQuery(); + } + + /** + * Builds a new WildcardQuery instance + * @param t wildcard term + * @return new WildcardQuery instance + */ + protected Query newWildcardQuery(Term t) { + WildcardQuery query = new WildcardQuery(t); + query.setRewriteMethod(multiTermRewriteMethod); + return query; + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + * @deprecated use {@link #getBooleanQuery(List)} instead + */ + protected Query getBooleanQuery(Vector clauses) throws ParseException { + return getBooleanQuery((List) clauses, false); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + */ + protected Query getBooleanQuery(List clauses) throws ParseException { + return getBooleanQuery(clauses, false); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * @param disableCoord true if coord scoring should be disabled. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + * @deprecated use {@link #getBooleanQuery(List, boolean)} instead + */ + protected Query getBooleanQuery(Vector clauses, boolean disableCoord) + throws ParseException + { + return getBooleanQuery((List) clauses, disableCoord); + } + + /** + * Factory method for generating query, given a set of clauses. + * By default creates a boolean query composed of clauses passed in. + * + * Can be overridden by extending classes, to modify query being + * returned. + * + * @param clauses List that contains {@link BooleanClause} instances + * to join. + * @param disableCoord true if coord scoring should be disabled. + * + * @return Resulting {@link Query} object. + * @exception ParseException throw in overridden method to disallow + */ + protected Query getBooleanQuery(List clauses, boolean disableCoord) + throws ParseException + { + if (clauses.size()==0) { + return null; // all clause words were filtered away by the analyzer. + } + BooleanQuery query = newBooleanQuery(disableCoord); + for (int i = 0; i < clauses.size(); i++) { + query.add((BooleanClause)clauses.get(i)); + } + return query; + } + + /** + * Factory method for generating a query. Called when parser + * parses an input term token that contains one or more wildcard + * characters (? and *), but is not a prefix term token (one + * that has just a single * character at the end) + *

+ * Depending on settings, prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

+ * Can be overridden by extending classes, to provide custom handling for + * wildcard queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token that contains one or more wild card + * characters (? or *), but is not simple prefix term + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getWildcardQuery(String field, String termStr) throws ParseException + { + if ("*".equals(field)) { + if ("*".equals(termStr)) return newMatchAllDocsQuery(); + } + if (!allowLeadingWildcard && (termStr.startsWith("*") || termStr.startsWith("?"))) + throw new ParseException("'*' or '?' not allowed as first character in WildcardQuery"); + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newWildcardQuery(t); + } + + /** + * Factory method for generating a query (similar to + * {@link #getWildcardQuery}). Called when parser parses an input term + * token that uses prefix notation; that is, contains a single '*' wildcard + * character as its last character. Since this is a special case + * of generic wildcard term, and such a query can be optimized easily, + * this usually results in a different query object. + *

+ * Depending on settings, a prefix term may be lower-cased + * automatically. It will not go through the default Analyzer, + * however, since normal Analyzers are unlikely to work properly + * with wildcard templates. + *

+ * Can be overridden by extending classes, to provide custom handling for + * wild card queries, which may be necessary due to missing analyzer calls. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * (without trailing '*' character!) + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getPrefixQuery(String field, String termStr) throws ParseException + { + if (!allowLeadingWildcard && termStr.startsWith("*")) + throw new ParseException("'*' not allowed as first character in PrefixQuery"); + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newPrefixQuery(t); + } + + /** + * Factory method for generating a query (similar to + * {@link #getWildcardQuery}). Called when parser parses + * an input term token that has the fuzzy suffix (~) appended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getFuzzyQuery(String field, String termStr, float minSimilarity) throws ParseException + { + if (lowercaseExpandedTerms) { + termStr = termStr.toLowerCase(); + } + Term t = new Term(field, termStr); + return newFuzzyQuery(t, minSimilarity, fuzzyPrefixLength); + } + + /** + * Returns a String where the escape char has been + * removed, or kept only once if there was a double escape. + * + * Supports escaped unicode characters, e. g. translates + * \\u0041 to A. + * + */ + private String discardEscapeChar(String input) throws ParseException { + // Create char array to hold unescaped char sequence + char[] output = new char[input.length()]; + + // The length of the output can be less than the input + // due to discarded escape chars. This variable holds + // the actual length of the output + int length = 0; + + // We remember whether the last processed character was + // an escape character + boolean lastCharWasEscapeChar = false; + + // The multiplier the current unicode digit must be multiplied with. + // E. g. the first digit must be multiplied with 16^3, the second with 16^2... + int codePointMultiplier = 0; + + // Used to calculate the codepoint of the escaped unicode character + int codePoint = 0; + + for (int i = 0; i < input.length(); i++) { + char curChar = input.charAt(i); + if (codePointMultiplier > 0) { + codePoint += hexToInt(curChar) * codePointMultiplier; + codePointMultiplier >>>= 4; + if (codePointMultiplier == 0) { + output[length++] = (char)codePoint; + codePoint = 0; + } + } else if (lastCharWasEscapeChar) { + if (curChar == 'u') { + // found an escaped unicode character + codePointMultiplier = 16 * 16 * 16; + } else { + // this character was escaped + output[length] = curChar; + length++; + } + lastCharWasEscapeChar = false; + } else { + if (curChar == '\\') { + lastCharWasEscapeChar = true; + } else { + output[length] = curChar; + length++; + } + } + } + + if (codePointMultiplier > 0) { + throw new ParseException("Truncated unicode escape sequence."); + } + + if (lastCharWasEscapeChar) { + throw new ParseException("Term can not end with escape character."); + } + + return new String(output, 0, length); + } + + /** Returns the numeric value of the hexadecimal character */ + private static final int hexToInt(char c) throws ParseException { + if ('0' <= c && c <= '9') { + return c - '0'; + } else if ('a' <= c && c <= 'f'){ + return c - 'a' + 10; + } else if ('A' <= c && c <= 'F') { + return c - 'A' + 10; + } else { + throw new ParseException("None-hex character in unicode escape sequence: " + c); + } + } + + /** + * Returns a String where those characters that QueryParser + * expects to be escaped are escaped by a preceding \. + */ + public static String escape(String s) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // These characters are part of the query syntax and must be escaped + if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':' + || c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~' + || c == '*' || c == '?' || c == '|' || c == '&') { + sb.append('\\'); + } + sb.append(c); + } + return sb.toString(); + } + + /** + * Command line tool to test QueryParser, using {@link org.apache.lucene.analysis.SimpleAnalyzer}. + * Usage:
+ * java org.apache.lucene.queryParser.QueryParser <input> + */ + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.out.println("Usage: java org.apache.lucene.queryParser.QueryParser "); + System.exit(0); + } + QueryParser qp = new QueryParser(Version.LUCENE_CURRENT, "field", + new org.apache.lucene.analysis.SimpleAnalyzer()); + Query q = qp.parse(args[0]); + System.out.println(q.toString("field")); + } +} + +PARSER_END(QueryParser) + +/* ***************** */ +/* Token Definitions */ +/* ***************** */ + +<*> TOKEN : { + <#_NUM_CHAR: ["0"-"9"] > +// every character that follows a backslash is considered as an escaped character +| <#_ESCAPED_CHAR: "\\" ~[] > +| <#_TERM_START_CHAR: ( ~[ " ", "\t", "\n", "\r", "\u3000", "+", "-", "!", "(", ")", ":", "^", + "[", "]", "\"", "{", "}", "~", "*", "?", "\\" ] + | <_ESCAPED_CHAR> ) > +| <#_TERM_CHAR: ( <_TERM_START_CHAR> | <_ESCAPED_CHAR> | "-" | "+" ) > +| <#_WHITESPACE: ( " " | "\t" | "\n" | "\r" | "\u3000") > +| <#_QUOTED_CHAR: ( ~[ "\"", "\\" ] | <_ESCAPED_CHAR> ) > +} + + SKIP : { + < <_WHITESPACE>> +} + + TOKEN : { + +| +| +| +| +| +| +| +| +| : Boost +| )* "\""> +| )* "'"> +| (<_TERM_CHAR>)* > +| )+ ( "." (<_NUM_CHAR>)+ )? )? > +| (<_TERM_CHAR>)* "*" ) > +| | [ "*", "?" ]) (<_TERM_CHAR> | ( [ "*", "?" ] ))* > +| : RangeIn +| : RangeEx +} + + TOKEN : { +)+ ( "." (<_NUM_CHAR>)+ )? > : DEFAULT +} + + TOKEN : { + +| : DEFAULT +| +| +} + + TOKEN : { + +| : DEFAULT +| +| +} + +// * Query ::= ( Clause )* +// * Clause ::= ["+", "-"] [ ":"] ( | "(" Query ")" ) + +int Conjunction() : { + int ret = CONJ_NONE; +} +{ + [ + { ret = CONJ_AND; } + | { ret = CONJ_OR; } + ] + { return ret; } +} + +int Modifiers() : { + int ret = MOD_NONE; +} +{ + [ + { ret = MOD_REQ; } + | { ret = MOD_NOT; } + | { ret = MOD_NOT; } + ] + { return ret; } +} + +// This makes sure that there is no garbage after the query string +Query TopLevelQuery(String field) : +{ + Query q; +} +{ + q=Query(field) + { + return q; + } +} + +Query Query(String field) : +{ + List clauses = new ArrayList(); + Query q, firstQuery=null; + int conj, mods; +} +{ + mods=Modifiers() q=Clause(field) + { + addClause(clauses, CONJ_NONE, mods, q); + if (mods == MOD_NONE) + firstQuery=q; + } + ( + conj=Conjunction() mods=Modifiers() q=Clause(field) + { addClause(clauses, conj, mods, q); } + )* + { + if (clauses.size() == 1 && firstQuery != null) + return firstQuery; + else { + return getBooleanQuery(clauses); + } + } +} + +Query Clause(String field) : { + Query q; + Token fieldToken=null, boost=null; +} +{ + [ + LOOKAHEAD(2) + ( + fieldToken= {field=discardEscapeChar(fieldToken.image);} + | {field="*";} + ) + ] + + ( + q=Term(field) + | q=Query(field) ( boost=)? + + ) + { + if (boost != null) { + float f = (float)1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + q.setBoost(f); + } catch (Exception ignored) { } + } + return q; + } +} + + +Query Term(String field) : { + Token term, boost=null, fuzzySlop=null, goop1, goop2; + boolean prefix = false; + boolean wildcard = false; + boolean fuzzy = false; + Query q; +} +{ + ( + ( + term= + | term= { wildcard=true; } + | term= { prefix=true; } + | term= { wildcard=true; } + | term= + ) + [ fuzzySlop= { fuzzy=true; } ] + [ boost= [ fuzzySlop= { fuzzy=true; } ] ] + { + String termImage=discardEscapeChar(term.image); + if (wildcard) { + q = getWildcardQuery(field, termImage); + } else if (prefix) { + q = getPrefixQuery(field, + discardEscapeChar(term.image.substring + (0, term.image.length()-1))); + } else if (fuzzy) { + float fms = fuzzyMinSim; + try { + fms = Float.valueOf(fuzzySlop.image.substring(1)).floatValue(); + } catch (Exception ignored) { } + if(fms < 0.0f || fms > 1.0f){ + throw new ParseException("Minimum similarity for a FuzzyQuery has to be between 0.0f and 1.0f !"); + } + q = getFuzzyQuery(field, termImage,fms); + } else { + q = getFieldQuery(field, termImage); + } + } + | ( ( goop1=|goop1= ) + [ ] ( goop2=|goop2= ) + ) + [ boost= ] + { + if (goop1.kind == RANGEIN_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } + if (goop2.kind == RANGEIN_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } + q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), true); + } + | ( ( goop1=|goop1= ) + [ ] ( goop2=|goop2= ) + ) + [ boost= ] + { + if (goop1.kind == RANGEEX_QUOTED) { + goop1.image = goop1.image.substring(1, goop1.image.length()-1); + } + if (goop2.kind == RANGEEX_QUOTED) { + goop2.image = goop2.image.substring(1, goop2.image.length()-1); + } + + q = getRangeQuery(field, discardEscapeChar(goop1.image), discardEscapeChar(goop2.image), false); + } + | term= + [ fuzzySlop= ] + [ boost= ] + { + int s = phraseSlop; + + if (fuzzySlop != null) { + try { + s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { } + } + q = getFieldQuery(field, discardEscapeChar(term.image.substring(1, term.image.length()-1)), s); + } + | term= + [ fuzzySlop= ] + [ boost= ] + { + int s = phraseSlop; + + if (fuzzySlop != null) { + try { + s = Float.valueOf(fuzzySlop.image.substring(1)).intValue(); + } + catch (Exception ignored) { } + } + q = getFieldQuery(field, discardEscapeChar(term.image.substring(0, term.image.length())), s); + } + ) + { + if (boost != null) { + float f = (float) 1.0; + try { + f = Float.valueOf(boost.image).floatValue(); + } + catch (Exception ignored) { + /* Should this be handled somehow? (defaults to "no boost", if + * boost number is invalid) + */ + } + + // avoid boosting null queries, such as those caused by stop words + if (q != null) { + q.setBoost(f); + } + } + return q; + } +} diff --git a/src/java/org/apache/lucene/queryParser/Token.java b/src/java/org/apache/lucene/queryParser/Token.java new file mode 100644 index 000000000..2bac4905a --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/Token.java @@ -0,0 +1,131 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 5.0 */ +/* JavaCCOptions:TOKEN_EXTENDS=,KEEP_LINE_COL=null,SUPPORT_CLASS_VISIBILITY_PUBLIC=true */ +package org.apache.lucene.queryParser; + +/** + * Describes the input token stream. + */ + +public class Token implements java.io.Serializable { + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** The line number of the first character of this Token. */ + public int beginLine; + /** The column number of the first character of this Token. */ + public int beginColumn; + /** The line number of the last character of this Token. */ + public int endLine; + /** The column number of the last character of this Token. */ + public int endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * An optional attribute value of the Token. + * Tokens which are not used as syntactic sugar will often contain + * meaningful values that will be used later on by the compiler or + * interpreter. This attribute value is often different from the image. + * Any subclass of Token that actually wants to return a non-null value can + * override this method as appropriate. + */ + public Object getValue() { + return null; + } + + /** + * No-argument constructor + */ + public Token() {} + + /** + * Constructs a new token for the specified Image. + */ + public Token(int kind) + { + this(kind, null); + } + + /** + * Constructs a new token for the specified Image and Kind. + */ + public Token(int kind, String image) + { + this.kind = kind; + this.image = image; + } + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simply add something like : + * + * case MyParserConstants.ID : return new IDToken(ofKind, image); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use sit in your lexical actions. + */ + public static Token newToken(int ofKind, String image) + { + switch(ofKind) + { + default : return new Token(ofKind, image); + } + } + + public static Token newToken(int ofKind) + { + return newToken(ofKind, null); + } + +} +/* JavaCC - OriginalChecksum=da95d0ec7daad286fab4e748b17294d8 (do not edit this line) */ diff --git a/src/java/org/apache/lucene/queryParser/TokenMgrError.java b/src/java/org/apache/lucene/queryParser/TokenMgrError.java new file mode 100644 index 000000000..6b2243ab1 --- /dev/null +++ b/src/java/org/apache/lucene/queryParser/TokenMgrError.java @@ -0,0 +1,147 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 5.0 */ +/* JavaCCOptions: */ +package org.apache.lucene.queryParser; + +/** Token Manager Error. */ +public class TokenMgrError extends Error +{ + + /** + * The version identifier for this Serializable class. + * Increment only if the serialized form of the + * class changes. + */ + private static final long serialVersionUID = 1L; + + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occurred. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt was made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their escaped (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexical error + * curLexState : lexical state in which this error occurred + * errorLine : line number when the error occurred + * errorColumn : column number when the error occurred + * errorAfter : prefix that was seen before this error occurred + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + /** No arg constructor. */ + public TokenMgrError() { + } + + /** Constructor with message and reason. */ + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + /** Full Constructor. */ + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} +/* JavaCC - OriginalChecksum=03df10dce345f1870429faa756473d14 (do not edit this line) */ diff --git a/src/java/org/apache/solr/handler/InvenioHandler.java b/src/java/org/apache/solr/handler/InvenioHandler.java new file mode 100644 index 000000000..27db6b71f --- /dev/null +++ b/src/java/org/apache/solr/handler/InvenioHandler.java @@ -0,0 +1,47 @@ +package org.apache.solr.handler; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.handler.component.SearchHandler; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.solr.util.WebUtils; + + + +public class InvenioHandler extends SearchHandler { + + public static final Logger log = LoggerFactory + .getLogger(InvenioHandler.class); + + + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) + throws Exception { + SolrParams params = req.getParams(); + String q = params.get(CommonParams.Q); + + // get the invenio parameters and set them into the request + String invParams = params.get("inv.params"); + Map qs = null; + if (invParams != null) { + qs = WebUtils.parseQueryString(invParams); + } + else { + log.warn("Received no parameters from Invenio (inv.params)"); + qs = new HashMap(); + } + Map context = req.getContext(); + context.put("inv.params", qs); + + + super.handleRequestBody(req, rsp); + } + + +} diff --git a/src/java/org/apache/solr/handler/PythonDiagnosticHandler.java b/src/java/org/apache/solr/handler/PythonDiagnosticHandler.java new file mode 100644 index 000000000..67fa18d0b --- /dev/null +++ b/src/java/org/apache/solr/handler/PythonDiagnosticHandler.java @@ -0,0 +1,156 @@ +package org.apache.solr.handler; + +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Map; + +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.core.SolrCore; +import org.apache.solr.handler.component.SearchHandler; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; +import org.apache.solr.request.SolrRequestHandler; +import org.apache.solr.search.DocSlice; +import org.apache.solr.util.DictionaryCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + + + +public class PythonDiagnosticHandler extends SearchHandler { + + public static final Logger log = LoggerFactory + .getLogger(PythonDiagnosticHandler.class); + + + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) + throws Exception { + SolrParams params = req.getParams(); + String q = params.get(CommonParams.Q); + + log.info("======= start diagnostics ======="); + + PythonMessage message = MontySolrVM.INSTANCE + .createMessage("diagnostic_test") + .setParam("query", q); + + try { + MontySolrVM.INSTANCE.sendMessage(message); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new IOException("Error searching Invenio!"); + } + + Object result = message.getResults(); + if (result != null) { + String res = (String) result; + rsp.add("diagnostic_message", res); + log.info("Diagnostic message: \n" + res); + } + else { + log.info("Diagnostic message: null"); + } + + // run invenio querys + String[] queries = {"boson", "title:boson", "inv_title:boson", "a*", "{!iq iq.mode=maxinv}title:boson", "year:1->99999999"}; + SolrCore core = req.getCore(); + SolrRequestHandler handler = core.getRequestHandler( "/invenio" ); + String qu = null; + Object pyresult = null; + int[] recids = null; + String r1 = null; + String r2 = null; + + Map recidToDocid = DictionaryCache.INSTANCE.getTranslationCache(req.getSearcher().getReader(), + req.getSchema().getUniqueKeyField().getName()); + + for (int i=0; i 0) { + rinfo += "[0]=" + lCache[0] + ", [" + d + "]=" + lCache[d] + ", [" + (lCache.length-1) + "]=" + lCache[lCache.length-1]; + } + else { + rinfo += " the cache is empty. You should visit /invenio_update"; + } + rsp.add("recids", rinfo); + log.info(rinfo); + log.info("======== end diagnostics ========"); + + } + + +} diff --git a/src/java/org/apache/solr/handler/component/InvenioFormatter.java b/src/java/org/apache/solr/handler/component/InvenioFormatter.java new file mode 100644 index 000000000..56dde168a --- /dev/null +++ b/src/java/org/apache/solr/handler/component/InvenioFormatter.java @@ -0,0 +1,176 @@ +package org.apache.solr.handler.component; + +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.lucene.search.Query; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; +import org.apache.solr.search.DocIterator; +import org.apache.solr.search.DocList; +import org.apache.solr.search.DocListAndSet; +import org.apache.solr.search.DocSlice; +import org.apache.solr.search.SolrIndexReader; +import org.apache.solr.search.SolrIndexSearcher.QueryCommand; +import org.apache.solr.search.SortSpec; +import org.apache.solr.util.DictionaryCache; + + +public class InvenioFormatter extends SearchComponent +{ + public static final String COMPONENT_NAME = "invenio-formatter"; + private boolean activated = false; + private Map invParams = null; + + @Override + public void prepare(ResponseBuilder rb) throws IOException { + activated = false; + SolrParams params = rb.req.getParams(); + Map context = rb.req.getContext(); + + if (context.containsKey("inv.params")) { + invParams = (Map) context.get("inv.params"); + if (invParams.containsKey("of")) { + String of = invParams.get("of"); + if (of.equals("hcs")) { // citation summary + ModifiableSolrParams rawParams = new ModifiableSolrParams(rb.req.getParams()); + Integer old_limit = params.getInt("rows", 10); + int max_len = params.getInt("inv.rows", 25000); + rawParams.set("rows", max_len); + rawParams.set("old_rows", old_limit); + rb.req.setParams(rawParams); + SortSpec sortSpec = rb.getSortSpec(); + SortSpec nss = new SortSpec(sortSpec.getSort(), sortSpec.getOffset(), max_len); + rb.setSortSpec(nss); + activated = true; + } + else if(invParams.containsKey("rm") && ((String)invParams.get("rm")).length() > 0) { + activated = true; + } + else if(invParams.containsKey("sf") && ((String)invParams.get("sf")).length() > 0) { + activated = true; + } + } + } + + } + + @Override + public void process(ResponseBuilder rb) throws IOException { + + if ( activated ) { + SolrParams params = rb.req.getParams(); + Integer original_limit = params.getInt("old_rows", params.getInt("rows")); + + SolrQueryRequest req = rb.req; + SolrQueryResponse rsp = rb.rsp; + + DocListAndSet results = rb.getResults(); + DocList dl = results.docList; + + if (dl.size() < 1) { + return; + } + + int[] recids = new int[dl.size()]; + DocIterator it = dl.iterator(); + + SolrIndexReader reader = rb.req.getSearcher().getReader(); + int[] docidMap = DictionaryCache.INSTANCE.getLuceneCache(reader, "id"); + + // translate into Invenio ID's + for (int i=0;it.hasNext();i++) { + recids[i] = docidMap[it.next()]; + } + + PythonMessage message = MontySolrVM.INSTANCE.createMessage("sort_and_format") + .setSender("InvenioFormatter") + .setSolrQueryRequest(req) + .setSolrQueryResponse(rsp) + .setParam("recids", recids) + .setParam("kwargs", invParams); + + try { + MontySolrVM.INSTANCE.sendMessage(message); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + Object result = message.getResults(); + String t = (String) message.getParam("rtype"); + if (result != null && t.contains("str")) { + rb.rsp.add("inv_response", (String)result); + + // truncate the number of retrieved documents back into reasonable size + int[] luceneIds = new int[original_limit>dl.size() ? dl.size() : original_limit]; + it = dl.iterator(); + for (int i=0;i recidToDocid = DictionaryCache.INSTANCE + .getTranslationCache(reader, rb.req.getSchema().getUniqueKeyField().getName()); + int[] recs = (int[]) result; + + // truncate the number of retrieved documents back into reasonable size + int[] luceneIds = new int[original_limit>recs.length ? recs.length : original_limit]; + for (int i=0;i ds, Map session) { + super(dataConfig, core, ds, session); + } + + + public boolean isBusy() { + return importLock.isLocked(); + } + + public void doFullImport(SolrWriter writer, RequestParams requestParams) { + LOG.info("Starting Full Import"); + setStatus(Status.RUNNING_FULL_DUMP); + + setIndexStartTime(new Date()); + + try { + docBuilder = new DocBuilder(this, writer, requestParams); + docBuilder.execute(); + if (!requestParams.debug) + cumulativeStatistics.add(docBuilder.importStatistics); + } catch (Throwable t) { + LOG.error("Full Import failed", t); + //docBuilder.rollback(); + } finally { + setStatus(Status.IDLE); + super.getConfig().clearCaches(); + DocBuilder.INSTANCE.set(null); + } + + } + + public void doDeltaImport(SolrWriter writer, RequestParams requestParams) { + LOG.info("Starting Delta Import"); + setStatus(Status.RUNNING_DELTA_DUMP); + + try { + setIndexStartTime(new Date()); + docBuilder = new DocBuilder(this, writer, requestParams); + docBuilder.execute(); + if (!requestParams.debug) + cumulativeStatistics.add(docBuilder.importStatistics); + } catch (Throwable t) { + LOG.error("Delta Import Failed", t); + //docBuilder.rollback(); + } finally { + setStatus(Status.IDLE); + super.getConfig().clearCaches(); + DocBuilder.INSTANCE.set(null); + } + + } + + void runCmd(RequestParams reqParams, SolrWriter sw) { + String command = reqParams.command; + if (command.equals(ABORT_CMD)) { + if (docBuilder != null) { + docBuilder.abort(); + } + return; + } + if (!importLock.tryLock()){ + LOG.warn("Import command failed . another import is running"); + return; + } + try { + if (FULL_IMPORT_CMD.equals(command) || IMPORT_CMD.equals(command)) { + doFullImport(sw, reqParams); + } else if (command.equals(DELTA_IMPORT_CMD)) { + doDeltaImport(sw, reqParams); + } + } finally { + importLock.unlock(); + } + } + + +} diff --git a/src/java/org/apache/solr/handler/dataimport/WaitingDataImportHandler.java b/src/java/org/apache/solr/handler/dataimport/WaitingDataImportHandler.java new file mode 100644 index 000000000..d40e4ba81 --- /dev/null +++ b/src/java/org/apache/solr/handler/dataimport/WaitingDataImportHandler.java @@ -0,0 +1,374 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.handler.dataimport; + +import static org.apache.solr.handler.dataimport.DataImporter.IMPORT_CMD; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.params.UpdateParams; +import org.apache.solr.common.util.ContentStreamBase; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.ContentStream; +import org.apache.solr.core.SolrConfig; +import org.apache.solr.core.SolrCore; +import org.apache.solr.core.SolrResourceLoader; +import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.handler.RequestHandlerUtils; +import org.apache.solr.request.RawResponseWriter; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; +import org.apache.solr.request.SolrRequestHandler; +import org.apache.solr.update.processor.UpdateRequestProcessor; +import org.apache.solr.update.processor.UpdateRequestProcessorChain; +import org.apache.solr.util.plugin.SolrCoreAware; + +import java.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

+ * Solr Request Handler for data import from databases and REST data sources. + *

+ *

+ * It is configured in solrconfig.xml + *

+ *

+ *

+ * Refer to http://wiki.apache.org/solr/DataImportHandler + * for more details. + *

+ *

+ * This API is experimental and subject to change + * + * @version $Id: DataImportHandler.java 788580 2009-06-26 05:20:23Z noble $ + * @since solr 1.3 + * + * NOTE: this is a slightly modified DataImportHandler that waits until the importer stops to be + * busy. + * + */ +public class WaitingDataImportHandler extends RequestHandlerBase implements + SolrCoreAware { + + private static final Logger LOG = LoggerFactory.getLogger(DataImportHandler.class); + + private DataImporter importer; + + private Map dataSources = new HashMap(); + + private List debugDocuments; + + private boolean debugEnabled = true; + + private String myName = "dataimport"; + + private Map coreScopeSession = new HashMap(); + + @Override + @SuppressWarnings("unchecked") + public void init(NamedList args) { + super.init(args); + } + + @SuppressWarnings("unchecked") + public void inform(SolrCore core) { + try { + //hack to get the name of this handler + for (Map.Entry e : core.getRequestHandlers().entrySet()) { + SolrRequestHandler handler = e.getValue(); + //this will not work if startup=lazy is set + if( this == handler) { + String name= e.getKey(); + if(name.startsWith("/")){ + myName = name.substring(1); + } + // some users may have '/' in the handler name. replace with '_' + myName = myName.replaceAll("/","_") ; + } + } + String debug = (String) initArgs.get(ENABLE_DEBUG); + if (debug != null && "no".equals(debug)) + debugEnabled = false; + NamedList defaults = (NamedList) initArgs.get("defaults"); + if (defaults != null) { + String configLoc = (String) defaults.get("config"); + if (configLoc != null && configLoc.length() != 0) { + processConfiguration(defaults); + + importer = new NoRollbackDataImporter(SolrWriter.getResourceAsString(core + .getResourceLoader().openResource(configLoc)), core, + dataSources, coreScopeSession); + } + } + } catch (Throwable e) { + SolrConfig.severeErrors.add(e); + LOG.error( DataImporter.MSG.LOAD_EXP, e); + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + DataImporter.MSG.INVALID_CONFIG, e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) + throws Exception { + rsp.setHttpCaching(false); + SolrParams params = req.getParams(); + DataImporter.RequestParams requestParams = new DataImporter.RequestParams(getParamsMap(params)); + String command = requestParams.command; + Iterable streams = req.getContentStreams(); + if(streams != null){ + for (ContentStream stream : streams) { + requestParams.contentStream = stream; + break; + } + } + if (DataImporter.SHOW_CONF_CMD.equals(command)) { + // Modify incoming request params to add wt=raw + ModifiableSolrParams rawParams = new ModifiableSolrParams(req.getParams()); + rawParams.set(CommonParams.WT, "raw"); + req.setParams(rawParams); + String dataConfigFile = defaults.get("config"); + ContentStreamBase content = new ContentStreamBase.StringStream(SolrWriter + .getResourceAsString(req.getCore().getResourceLoader().openResource( + dataConfigFile))); + rsp.add(RawResponseWriter.CONTENT, content); + return; + } + + rsp.add("initArgs", initArgs); + String message = ""; + + if (command != null) + rsp.add("command", command); + + if (requestParams.debug && (importer == null || !importer.isBusy())) { + // Reload the data-config.xml + importer = null; + if (requestParams.dataConfig != null) { + try { + processConfiguration((NamedList) initArgs.get("defaults")); + importer = new DataImporter(requestParams.dataConfig, req.getCore() + , dataSources, coreScopeSession); + } catch (RuntimeException e) { + rsp.add("exception", DebugLogger.getStacktraceString(e)); + importer = null; + return; + } + } else { + inform(req.getCore()); + } + message = DataImporter.MSG.CONFIG_RELOADED; + } + + // If importer is still null + if (importer == null) { + rsp.add("status", DataImporter.MSG.NO_INIT); + return; + } + + if (command != null && DataImporter.ABORT_CMD.equals(command)) { + importer.runCmd(requestParams, null); + } + else { + if (importer.isBusy()) { + while(true) { + Thread.sleep(30); + if (!importer.isBusy()) { + break; + } + } + } + + if (command != null) { + if (DataImporter.FULL_IMPORT_CMD.equals(command) + || DataImporter.DELTA_IMPORT_CMD.equals(command) || + IMPORT_CMD.equals(command)) { + + UpdateRequestProcessorChain processorChain = + req.getCore().getUpdateProcessingChain(params.get(UpdateParams.UPDATE_PROCESSOR)); + UpdateRequestProcessor processor = processorChain.createProcessor(req, rsp); + SolrResourceLoader loader = req.getCore().getResourceLoader(); + SolrWriter sw = getSolrWriter(processor, loader, requestParams); + + if (requestParams.debug) { + if (debugEnabled) { + // Synchronous request for the debug mode + importer.runCmd(requestParams, sw); + rsp.add("mode", "debug"); + rsp.add("documents", debugDocuments); + if (sw.debugLogger != null) + rsp.add("verbose-output", sw.debugLogger.output); + debugDocuments = null; + } else { + message = DataImporter.MSG.DEBUG_NOT_ENABLED; + } + } else { + // Asynchronous request for normal mode + if(requestParams.contentStream == null){ + importer.runAsync(requestParams, sw); + } else { + importer.runCmd(requestParams, sw); + } + } + } else if (DataImporter.RELOAD_CONF_CMD.equals(command)) { + importer = null; + inform(req.getCore()); + message = DataImporter.MSG.CONFIG_RELOADED; + } + } + } + rsp.add("status", importer.isBusy() ? "busy" : "idle"); + rsp.add("importResponse", message); + rsp.add("statusMessages", importer.getStatusMessages()); + + RequestHandlerUtils.addExperimentalFormatWarning(rsp); + } + + private Map getParamsMap(SolrParams params) { + Iterator names = params.getParameterNamesIterator(); + Map result = new HashMap(); + while (names.hasNext()) { + String s = names.next(); + String[] val = params.getParams(s); + if (val == null || val.length < 1) + continue; + if (val.length == 1) + result.put(s, val[0]); + else + result.put(s, Arrays.asList(val)); + } + return result; + } + + @SuppressWarnings("unchecked") + private void processConfiguration(NamedList defaults) { + if (defaults == null) { + LOG.info("No configuration specified in solrconfig.xml for DataImportHandler"); + return; + } + + LOG.info("Processing configuration from solrconfig.xml: " + defaults); + + dataSources = new HashMap(); + + int position = 0; + + while (position < defaults.size()) { + if (defaults.getName(position) == null) + break; + + String name = defaults.getName(position); + if (name.equals("datasource")) { + NamedList dsConfig = (NamedList) defaults.getVal(position); + Properties props = new Properties(); + for (int i = 0; i < dsConfig.size(); i++) + props.put(dsConfig.getName(i), dsConfig.getVal(i)); + LOG.info("Adding properties to datasource: " + props); + dataSources.put((String) dsConfig.get("name"), props); + } + position++; + } + } + + private SolrWriter getSolrWriter(final UpdateRequestProcessor processor, + final SolrResourceLoader loader, final DataImporter.RequestParams requestParams) { + + return new SolrWriter(processor, loader.getConfigDir(), myName) { + + @Override + public boolean upload(SolrInputDocument document) { + try { + if (requestParams.debug) { + if (debugDocuments == null) + debugDocuments = new ArrayList(); + debugDocuments.add(document); + } + return super.upload(document); + } catch (RuntimeException e) { + LOG.error( "Exception while adding: " + document, e); + return false; + } + } + }; + } + + @Override + @SuppressWarnings("unchecked") + public NamedList getStatistics() { + if (importer == null) + return super.getStatistics(); + + DocBuilder.Statistics cumulative = importer.cumulativeStatistics; + NamedList result = new NamedList(); + + result.add("Status", importer.getStatus().toString()); + + if (importer.docBuilder != null) { + DocBuilder.Statistics running = importer.docBuilder.importStatistics; + result.add("Documents Processed", running.docCount); + result.add("Requests made to DataSource", running.queryCount); + result.add("Rows Fetched", running.rowsCount); + result.add("Documents Deleted", running.deletedDocCount); + result.add("Documents Skipped", running.skipDocCount); + } + + result.add(DataImporter.MSG.TOTAL_DOC_PROCESSED, cumulative.docCount); + result.add(DataImporter.MSG.TOTAL_QUERIES_EXECUTED, cumulative.queryCount); + result.add(DataImporter.MSG.TOTAL_ROWS_EXECUTED, cumulative.rowsCount); + result.add(DataImporter.MSG.TOTAL_DOCS_DELETED, cumulative.deletedDocCount); + result.add(DataImporter.MSG.TOTAL_DOCS_SKIPPED, cumulative.skipDocCount); + + NamedList requestStatistics = super.getStatistics(); + if (requestStatistics != null) { + for (int i = 0; i < requestStatistics.size(); i++) { + result.add(requestStatistics.getName(i), requestStatistics.getVal(i)); + } + } + + return result; + } + + // //////////////////////SolrInfoMBeans methods ////////////////////// + + @Override + public String getDescription() { + return DataImporter.MSG.JMX_DESC; + } + + @Override + public String getSourceId() { + return "$Id: DataImportHandler.java 788580 2009-06-26 05:20:23Z noble $"; + } + + @Override + public String getVersion() { + return "1.0"; + } + + @Override + public String getSource() { + return "$URL: http://svn.apache.org/repos/asf/lucene/solr/tags/release-1.4.1/contrib/dataimporthandler/src/main/java/org/apache/solr/handler/dataimport/DataImportHandler.java $"; + } + + public static final String ENABLE_DEBUG = "enableDebug"; +} diff --git a/src/java/org/apache/solr/schema/FileResolverTextField.java b/src/java/org/apache/solr/schema/FileResolverTextField.java new file mode 100644 index 000000000..9fa88ac33 --- /dev/null +++ b/src/java/org/apache/solr/schema/FileResolverTextField.java @@ -0,0 +1,111 @@ + + +package org.apache.solr.schema; + +import org.apache.lucene.search.SortField; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Fieldable; +import org.apache.solr.request.XMLWriter; +import org.apache.solr.request.TextResponseWriter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** TextField is the basic type for configurable text analysis. + * Analyzers for field types using this implementation should be defined in the schema. + * @version $Id: TextField.java 764291 2009-04-12 11:03:09Z shalin $ + */ +public class FileResolverTextField extends CompressableField { + protected void init(IndexSchema schema, Map args) { + properties |= TOKENIZED; + if (schema.getVersion()> 1.1f) properties &= ~OMIT_TF_POSITIONS; + + super.init(schema, args); + } + + public SortField getSortField(SchemaField field, boolean reverse) { + return getStringSort(field, reverse); + } + + public void write(XMLWriter xmlWriter, String name, Fieldable f) throws IOException { + xmlWriter.writeStr(name, f.stringValue()); + } + + public void write(TextResponseWriter writer, String name, Fieldable f) throws IOException { + writer.writeStr(name, f.stringValue(), true); + } + + public Field createField(SchemaField field, String externalVal, float boost) { + //System.out.println(externalVal); + //String val = externalVal.toLowerCase() + " Hey!"; //null; + String val = null; + String[] vals = externalVal.split("\\|"); + Map values = new HashMap(); + for (String v: vals) { + if (v.indexOf(':') > 0) { + String[] parts = v.split(":", 0); + String p = parts[1]; + if (p.startsWith("[") && p.endsWith("]")) { + p = p.substring(1, p.length()-1); + } + values.put(parts[0], p); + } + } + if (values.containsKey("src_dir") && values.containsKey("arxiv_id")) { + String[] dirs = values.get("src_dir").split(","); + + String arx = values.get("arxiv_id"); + String fname = null; + String topdir = null; + + if (arx.indexOf('/') > -1) { + String[] arx_parts = arx.split("/", 0); //hep-th/0002162 + topdir = arx_parts[1].substring(0, 4); + fname = arx_parts[0] + arx_parts[1]; + } + else if(arx.indexOf(':') > -1) { + String[] arx_parts = arx.replace("arXiv:", "").split("\\.", 0); //arXiv:0712.0712 + topdir = arx_parts[0]; + fname = arx_parts[0] + '.' + arx_parts[1]; + } + + if (fname != null) { + File f = null; + for (String d: dirs) { + String s = d + "/" + topdir + "/" + fname; + f = new File(s + ".txt"); + if (f.exists()) { + StringBuilder text = new StringBuilder(); + String fEncoding = "UTF-8"; + String NL = System.getProperty("line.separator"); + Scanner scanner; + try { + scanner = new Scanner(new FileInputStream(f), fEncoding ); + try { + while (scanner.hasNextLine()){ + text.append(scanner.nextLine() + NL); + } + } + finally{ + scanner.close(); + val = text.toString(); + } + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + break; + } + } + } + }// value has src_dir and arxiv_id + + return super.createField(field, val, boost); + } +} diff --git a/src/java/org/apache/solr/schema/PythonTextField.java b/src/java/org/apache/solr/schema/PythonTextField.java new file mode 100644 index 000000000..10d00495a --- /dev/null +++ b/src/java/org/apache/solr/schema/PythonTextField.java @@ -0,0 +1,72 @@ + + +package org.apache.solr.schema; + +import org.apache.lucene.search.SortField; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Fieldable; +import org.apache.solr.request.XMLWriter; +import org.apache.solr.request.TextResponseWriter; + + +import invenio.montysolr.jni.PythonBridge; +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** TextField is the basic type for configurable text analysis. + * Analyzers for field types using this implementation should be defined in the schema. + * @version $Id: TextField.java 764291 2009-04-12 11:03:09Z shalin $ + */ +public class PythonTextField extends CompressableField { + protected void init(IndexSchema schema, Map args) { + properties |= TOKENIZED; + if (schema.getVersion()> 1.1f) properties &= ~OMIT_TF_POSITIONS; + + super.init(schema, args); + } + + public SortField getSortField(SchemaField field, boolean reverse) { + return getStringSort(field, reverse); + } + + public void write(XMLWriter xmlWriter, String name, Fieldable f) throws IOException { + xmlWriter.writeStr(name, f.stringValue()); + } + + public void write(TextResponseWriter writer, String name, Fieldable f) throws IOException { + writer.writeStr(name, f.stringValue(), true); + } + + public Field createField(SchemaField field, String externalVal, float boost) { + + //String val = bridge.workoutFieldValue(this.getClass().getName(), field, externalVal, boost); + PythonMessage message = MontySolrVM.INSTANCE.createMessage("workout_field_value") + .setSender("PythonTextField") + .setParam("field", field) + .setParam("externalVal", externalVal) + .setParam("boost", boost); + + try { + MontySolrVM.INSTANCE.sendMessage(message); + if (message.containsKey("result")) { + String val = (String) message.getResults(); + if (val != null) + return super.createField(field, val, boost); + } + } catch (InterruptedException e) { + // pass, we will not access the message object it may be + // in inconsistent state + } + + + return null; + } +} diff --git a/src/java/org/apache/solr/search/CitationQuery.java b/src/java/org/apache/solr/search/CitationQuery.java new file mode 100644 index 000000000..9a3604af3 --- /dev/null +++ b/src/java/org/apache/solr/search/CitationQuery.java @@ -0,0 +1,431 @@ +package org.apache.solr.search; + +import org.apache.jcc.PythonVM; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.DocIdSet; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.FieldCache; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.OpenBitSet; +import org.apache.lucene.util.ToStringUtils; + +import invenio.montysolr.jni.PythonBridge; +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; +import java.util.BitSet; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.MapFieldSelector; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.util.DictionaryCache; + + + +public class CitationQuery extends Query { + private float boost = 1.0f; // query boost factor + Query query; + SolrParams localParams; + SolrQueryRequest req; + String idField = "id"; //TODO: make it configurable + String dictName = null; + + + public CitationQuery (Query query, SolrQueryRequest req, SolrParams localParams) { + this.query = query; + this.localParams = localParams; + this.req = req; + + String type = localParams.get("rel"); + if (type.contains("refersto")) { + dictName = "citationdict"; + } + else if (type.contains("citedby")) { + dictName = "reversedict"; + } + else { + dictName = "reversedict"; + } + } + + /** + * Sets the boost for this query clause to b. Documents + * matching this clause will (in addition to the normal weightings) have + * their score multiplied by b. + */ + public void setBoost(float b) { + boost = b; + } + + /** + * Gets the boost for this clause. Documents matching this clause will (in + * addition to the normal weightings) have their score multiplied by + * b. The boost is 1.0 by default. + */ + public float getBoost() { + return boost + query.getBoost(); + } + + public Map getDictCache() throws IOException { + try { + return getDictCache(this.dictName); + } catch (InterruptedException e) { + e.printStackTrace(); + // return empty map, that is ok because it will affect only + // this query, the next will get a new cache + return new HashMap(); + } + } + + public Map getDictCache(String dictname) throws IOException, InterruptedException { + + + Map cache = DictionaryCache.INSTANCE.getCache(dictname); + + + if (cache == null) { + + + + // Get mapping lucene_id->invenio_recid + // The simplest would be to load the field with a cache (but the + // field should be integer - and it is not now). The other reason + // for doint this is that we don't create unnecessary cache + + /** + + TermDocs td = reader.termDocs(); //FIXME: .termDocs(new Term(idField)) works not?! + String[] li = {idField}; + MapFieldSelector fieldSelector = new MapFieldSelector(li); + **/ + + SolrIndexSearcher searcher = req.getSearcher(); + SolrIndexReader reader = searcher.getReader(); + int[] idMapping = FieldCache.DEFAULT.getInts(reader, idField); + + Map fromValueToDocid = new HashMap(idMapping.length); + int i = 0; + for (int value: idMapping) { + fromValueToDocid.put(value, i); + i++; + } + + /** + //OpenBitSet bitSet = new OpenBitSet(reader.maxDoc()); + int i; + while (td.next()) { + i = td.doc(); + // not needed when term is null + //if (reader.isDeleted(i)) { + // continue; + //} + Document doc = reader.document(i); + + try { + //bitSet.set(Integer.parseInt(doc.get(idField))); + idMap.put(i, Integer.parseInt(doc.get(idField))); + } catch (Exception e) { + e.printStackTrace(); + } + } + **/ + + // now get the citation dictionary from Invenio + HashMap hm = new HashMap(); + + PythonMessage message = MontySolrVM.INSTANCE.createMessage("get_citation_dict") + .setSender("CitationQuery") + .setParam("dictname", dictName) + .setParam("result", hm); + MontySolrVM.INSTANCE.sendMessage(message); + + + Map citationDict = new HashMap(0); + if (message.containsKey("result")) { + + Map result = (Map) message.getResults(); + citationDict = new HashMap(result.size()); + for (Entry e: result.entrySet()) { + Integer recid = e.getKey(); + if (fromValueToDocid.containsKey(recid)) { + // translate recids into lucene-ids + + int[] recIds = (int[]) e.getValue(); + int[] lucIds = new int[recIds.length]; + for (int x=0;x getDictCacheX() { + HashMap hm = new HashMap(); + + int Min = 1; + int Max = 5000; + int r; + for (int i=0;i<5000; i++) { + r = Min + (int)(Math.random() * ((Max - Min) + 1)); + BitSet bs = new BitSet(r); + int ii = 0; + while (ii < 20) { + r = Min + (int)(Math.random() * ((Max - Min) + 1)); + bs.set(r); + ii += 1; + } + hm.put(i, bs); + } + return hm; + } + + + /** + * Expert: Constructs an appropriate Weight implementation for this query. + * + *

+ * Only implemented by primitive queries, which re-write to themselves. + */ + public Weight createWeight(Searcher searcher) throws IOException { + + final Weight weight = query.createWeight (searcher); + final Similarity similarity = query.getSimilarity(searcher); + + + + return new Weight() { + private float value; + + + // return a filtering scorer + public Scorer scorer(IndexReader indexReader, boolean scoreDocsInOrder, boolean topScorer) + throws IOException { + + final Scorer scorer = weight.scorer(indexReader, true, false); + final IndexReader reader = indexReader; + + if (scorer == null) { + return null; + } + + + // we override the Scorer for the CitationQuery + return new Scorer(similarity) { + + private int doc = -1; + + public void getCache() { + System.out.println(reader); + + } + + // here is the core of the processing + public void score(Collector collector) throws IOException { + collector.setScorer(this); + int doc; + + //TODO: we could as well collect the first matching documents + //and based on them retrieve all the citations. But probably + //that is not correct, because the citation search wants to + //retrieve the documents that are most cited/referred, therefore + //we have to search the whole space + + // get the respective dictionary + Map cache = getDictCache(); + BitSet aHitSet = new BitSet(reader.maxDoc()); + + if (cache.size() == 0) + return; + + // retrieve documents that matched the query and while we go + // collect the documents referenced by/from those docs + while ((doc = nextDoc()) != NO_MORE_DOCS) { + if (cache.containsKey(doc)) { + int[] v = cache.get(doc); + for (int i: v) + aHitSet.set(i); + } + } + + // now collect the big set of citing relations + doc = 0; + while ((doc = aHitSet.nextSetBit(doc)) != -1) { + collector.collect(doc); + doc += 1; + } + } + + public int nextDoc() throws IOException { + return scorer.nextDoc(); + } + + /** @deprecated use {@link #docID()} instead. */ + public int doc() { return scorer.doc(); } + public int docID() { return doc; } + + /** @deprecated use {@link #advance(int)} instead. */ + public boolean skipTo(int i) throws IOException { + return advance(i) != NO_MORE_DOCS; + } + + public int advance(int target) throws IOException { + return scorer.advance(target); + } + + //public float score() throws IOException { return getBoost() * scorer.score(); } + public float score() throws IOException { return getBoost() * 1.0f; } + };// Scorer + }// scorer + + // pass these methods through to enclosed query's weight + public float getValue() { return value; } + + public float sumOfSquaredWeights() throws IOException { + return weight.sumOfSquaredWeights() * getBoost() * getBoost(); + } + + public void normalize (float v) { + weight.normalize(v); + value = weight.getValue() * getBoost(); + } + + public Explanation explain (IndexReader ir, int i) throws IOException { + Explanation inner = weight.explain (ir, i); + if (getBoost()!=1) { + Explanation preBoost = inner; + inner = new Explanation(inner.getValue()*getBoost(),"product of:"); + inner.addDetail(new Explanation(getBoost(),"boost")); + inner.addDetail(preBoost); + } + inner.addDetail(new Explanation(0.0f, "TODO: add citation formula details")); + return inner; + } + + // return this query + public Query getQuery() { return CitationQuery.this; } + + }; //Weight + } + + /** + * Expert: Constructs and initializes a Weight for a top-level query. + */ + public Weight weight(Searcher searcher) throws IOException { + Query query = searcher.rewrite(this); + Weight weight = query.createWeight(searcher); + float sum = weight.sumOfSquaredWeights(); + float norm = getSimilarity(searcher).queryNorm(sum); + if (Float.isInfinite(norm) || Float.isNaN(norm)) + norm = 1.0f; + weight.normalize(norm); + return weight; + } + + /** + * Expert: called to re-write queries into primitive queries. For example, a + * PrefixQuery will be rewritten into a BooleanQuery that consists of + * TermQuerys. + */ + public Query rewrite(IndexReader reader) throws IOException { + Query rewritten = query.rewrite(reader); + if (rewritten != query) { + CitationQuery clone = (CitationQuery)this.clone(); + clone.query = rewritten; + return clone; + } else { + return this; + } + } + + /** + * Expert: called when re-writing queries under MultiSearcher. + * + * Create a single query suitable for use by all subsearchers (in 1-1 + * correspondence with queries). This is an optimization of the OR of all + * queries. We handle the common optimization cases of equal queries and + * overlapping clauses of boolean OR queries (as generated by + * MultiTermQuery.rewrite()). Be careful overriding this method as + * queries[0] determines which method will be called and is not necessarily + * of the same type as the other queries. + */ + public Query combine(Query[] queries) { + return query.combine(queries); + + } + + /** + * Expert: adds all terms occurring in this query to the terms set. Only + * works if this query is in its {@link #rewrite rewritten} form. + * + * @throws UnsupportedOperationException + * if this query is not yet rewritten + */ + public void extractTerms(Set terms) { + query.extractTerms(terms); + } + + + /** + * Expert: Returns the Similarity implementation to be used for this query. + * Subclasses may override this method to specify their own Similarity + * implementation, perhaps one that delegates through that of the Searcher. + * By default the Searcher's Similarity implementation is returned. + */ + public Similarity getSimilarity(Searcher searcher) { + return searcher.getSimilarity(); + } + + + + /** Prints a user-readable version of this query. */ + public String toString (String s) { + StringBuffer buffer = new StringBuffer(); + buffer.append("CitationQuery("); + buffer.append(query.toString(s)); + buffer.append(")->"); + buffer.append(ToStringUtils.boost(getBoost())); + return buffer.toString(); + } + + /** Returns true iff o is equal to this. */ + public boolean equals(Object o) { + if (o instanceof CitationRefersToQuery) { + CitationRefersToQuery fq = (CitationRefersToQuery) o; + return (query.equals(fq.query) && getBoost()==fq.getBoost()); + } + return false; + } + + /** Returns a hash code value for this object. */ + public int hashCode() { + return query.hashCode() ^ Float.floatToRawIntBits(getBoost()); + } +} diff --git a/src/java/org/apache/solr/search/CitationRefersToQParserPlugin.java b/src/java/org/apache/solr/search/CitationRefersToQParserPlugin.java new file mode 100644 index 000000000..91472af83 --- /dev/null +++ b/src/java/org/apache/solr/search/CitationRefersToQParserPlugin.java @@ -0,0 +1,317 @@ +package org.apache.solr.search; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.DocIdSet; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.lucene.search.FilteredQuery; +import org.apache.lucene.util.OpenBitSet; +import org.apache.lucene.util.ToStringUtils; +import org.apache.solr.search.CitationQuery; + +/** + * Parse Invenio's variant on the refersto citation
+ * Other parameters: + *

    + *
  • q.op - the default operator "OR" or "AND"
  • + *
  • df - the default field name
  • + *
+ *
+ * Example: + * {!relation q.op=AND df=author sort='price asc'}coauthor:ellis +bar -baz + */ +public class CitationRefersToQParserPlugin extends QParserPlugin { + public static String NAME = "refersto"; + + @Override + public void init(NamedList args) { + } + + @Override + public QParser createParser(String qstr, SolrParams localParams, + SolrParams params, SolrQueryRequest req) { + return new InvenioRefersToQParser(qstr, localParams, params, req); + } + +} + +class InvenioRefersToQParser extends QParser { + String sortStr; + SolrQueryParser lparser; + + public InvenioRefersToQParser(String qstr, SolrParams localParams, + SolrParams params, SolrQueryRequest req) { + super(qstr, localParams, params, req); + } + + public Query parse() throws ParseException { + String qstr = getString(); + + String defaultField = getParam(CommonParams.DF); + if (defaultField == null) { + defaultField = getReq().getSchema().getDefaultSearchFieldName(); + } + lparser = new SolrQueryParser(this, defaultField); + + // these could either be checked & set here, or in the SolrQueryParser + // constructor + String opParam = getParam(QueryParsing.OP); + if (opParam != null) { + lparser.setDefaultOperator("AND".equals(opParam) ? QueryParser.Operator.AND + : QueryParser.Operator.OR); + } else { + // try to get default operator from schema + QueryParser.Operator operator = getReq().getSchema() + .getSolrQueryParser(null).getDefaultOperator(); + lparser.setDefaultOperator(null == operator ? QueryParser.Operator.OR + : operator); + } + + Query mainq = lparser.parse(qstr); + //Filter qfilter = new CitationRefersToFilter(); + + //return new CitationRefersToQuery(mainq, qfilter); + return new CitationQuery(mainq, req, localParams); + } + + public String[] getDefaultHighlightFields() { + return new String[] { lparser.getField() }; + } + +} + +class CitationRefersToQuery +extends Query { + + Query query; + Filter filter; + + /** + * Constructs a new query which applies a filter to the results of the original query. + * Filter.getDocIdSet() will be called every time this query is used in a search. + * @param query Query to be filtered, cannot be null. + * @param filter Filter to apply to query results, cannot be null. + */ + public CitationRefersToQuery (Query query, Filter filter) { + this.query = query; + this.filter = filter; + } + + /** + * Returns a Weight that applies the filter to the enclosed query's Weight. + * This is accomplished by overriding the Scorer returned by the Weight. + */ + public Weight createWeight(final Searcher searcher) throws IOException { + final Weight weight = query.createWeight (searcher); + final Similarity similarity = query.getSimilarity(searcher); + return new Weight() { + private float value; + + // pass these methods through to enclosed query's weight + public float getValue() { return value; } + public float sumOfSquaredWeights() throws IOException { + return weight.sumOfSquaredWeights() * getBoost() * getBoost(); + } + public void normalize (float v) { + weight.normalize(v); + value = weight.getValue() * getBoost(); + } + public Explanation explain (IndexReader ir, int i) throws IOException { + Explanation inner = weight.explain (ir, i); + if (getBoost()!=1) { + Explanation preBoost = inner; + inner = new Explanation(inner.getValue()*getBoost(),"product of:"); + inner.addDetail(new Explanation(getBoost(),"boost")); + inner.addDetail(preBoost); + } + Filter f = CitationRefersToQuery.this.filter; + DocIdSet docIdSet = f.getDocIdSet(ir); + DocIdSetIterator docIdSetIterator = docIdSet == null ? DocIdSet.EMPTY_DOCIDSET.iterator() : docIdSet.iterator(); + if (docIdSetIterator == null) { + docIdSetIterator = DocIdSet.EMPTY_DOCIDSET.iterator(); + } + if (docIdSetIterator.advance(i) == i) { + return inner; + } else { + Explanation result = new Explanation + (0.0f, "failure to match filter: " + f.toString()); + result.addDetail(inner); + return result; + } + } + + // return this query + public Query getQuery() { return CitationRefersToQuery.this; } + + // return a filtering scorer + public Scorer scorer(IndexReader indexReader, boolean scoreDocsInOrder, boolean topScorer) + throws IOException { + final Scorer scorer = weight.scorer(indexReader, true, false); + if (scorer == null) { + return null; + } + DocIdSet docIdSet = filter.getDocIdSet(indexReader); + if (docIdSet == null) { + return null; + } + final DocIdSetIterator docIdSetIterator = docIdSet.iterator(); + if (docIdSetIterator == null) { + return null; + } + + return new Scorer(similarity) { + + private int doc = -1; + + private int advanceToCommon(int scorerDoc, int disiDoc) throws IOException { + while (scorerDoc != disiDoc) { + if (scorerDoc < disiDoc) { + scorerDoc = scorer.advance(disiDoc); + } else { + disiDoc = docIdSetIterator.advance(scorerDoc); + } + } + return scorerDoc; + } + + public void score(Collector collector) throws IOException { + collector.setScorer(this); + int doc; + while ((doc = nextDoc()) != NO_MORE_DOCS) { + collector.collect(doc); + } + } + + /** @deprecated use {@link #nextDoc()} instead. */ + public boolean next() throws IOException { + return nextDoc() != NO_MORE_DOCS; + } + + public int nextDoc() throws IOException { + int scorerDoc, disiDoc; + return doc = (disiDoc = docIdSetIterator.nextDoc()) != NO_MORE_DOCS + && (scorerDoc = scorer.nextDoc()) != NO_MORE_DOCS + && advanceToCommon(scorerDoc, disiDoc) != NO_MORE_DOCS ? scorer.docID() : NO_MORE_DOCS; + } + + /** @deprecated use {@link #docID()} instead. */ + public int doc() { return scorer.doc(); } + public int docID() { return doc; } + + /** @deprecated use {@link #advance(int)} instead. */ + public boolean skipTo(int i) throws IOException { + return advance(i) != NO_MORE_DOCS; + } + + public int advance(int target) throws IOException { + int disiDoc, scorerDoc; + return doc = (disiDoc = docIdSetIterator.advance(target)) != NO_MORE_DOCS + && (scorerDoc = scorer.advance(disiDoc)) != NO_MORE_DOCS + && advanceToCommon(scorerDoc, disiDoc) != NO_MORE_DOCS ? scorer.docID() : NO_MORE_DOCS; + } + + public float score() throws IOException { return getBoost() * scorer.score(); } + + // add an explanation about whether the document was filtered + public Explanation explain (int i) throws IOException { + Explanation exp = scorer.explain(i); + + if (docIdSetIterator.advance(i) == i) { + exp.setDescription ("allowed by filter: "+exp.getDescription()); + exp.setValue(getBoost() * exp.getValue()); + } else { + exp.setDescription ("removed by filter: "+exp.getDescription()); + exp.setValue(0.0f); + } + return exp; + } + }; + } + }; + } + + /** Rewrites the wrapped query. */ + public Query rewrite(IndexReader reader) throws IOException { + Query rewritten = query.rewrite(reader); + if (rewritten != query) { + CitationRefersToQuery clone = (CitationRefersToQuery)this.clone(); + clone.query = rewritten; + return clone; + } else { + return this; + } + } + + public Query getQuery() { + return query; + } + + public Filter getFilter() { + return filter; + } + + // inherit javadoc + public void extractTerms(Set terms) { + getQuery().extractTerms(terms); + } + + /** Prints a user-readable version of this query. */ + public String toString (String s) { + StringBuffer buffer = new StringBuffer(); + buffer.append("filtered("); + buffer.append(query.toString(s)); + buffer.append(")->"); + buffer.append(filter); + buffer.append(ToStringUtils.boost(getBoost())); + return buffer.toString(); + } + + /** Returns true iff o is equal to this. */ + public boolean equals(Object o) { + if (o instanceof CitationRefersToQuery) { + CitationRefersToQuery fq = (CitationRefersToQuery) o; + return (query.equals(fq.query) && filter.equals(fq.filter) && getBoost()==fq.getBoost()); + } + return false; + } + + /** Returns a hash code value for this object. */ + public int hashCode() { + return query.hashCode() ^ filter.hashCode() + Float.floatToRawIntBits(getBoost()); + } + } + +class CitationRefersToFilter extends Filter { + + /** + * This method returns a set of documents that are referring (citing) + * the set of documents we retrieved in the underlying query + */ + @Override + public DocIdSet getDocIdSet(IndexReader reader) throws IOException { + final OpenBitSet bitSet = new OpenBitSet(reader.maxDoc()); + for (int i=0; i < reader.maxDoc(); i++) { + bitSet.set(i); + } + return bitSet; + } + + +} diff --git a/src/java/org/apache/solr/search/InvenioQParserPlugin.java b/src/java/org/apache/solr/search/InvenioQParserPlugin.java new file mode 100644 index 000000000..b772502e4 --- /dev/null +++ b/src/java/org/apache/solr/search/InvenioQParserPlugin.java @@ -0,0 +1,543 @@ +package org.apache.solr.search; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.lucene.analysis.WhitespaceAnalyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.queryParser.InvenioQueryParser; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.DefaultSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.schema.FieldType; +import org.apache.solr.schema.IndexSchema; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.ConstantScoreQuery; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.NumericRangeQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.WildcardQuery; +import org.apache.lucene.util.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.solr.search.QueryParsing; +import org.apache.solr.util.DictionaryCache; + + + +/** + * Parse query that is made of the solr fields as well as Invenio query syntax, + * the field that are prefixed using the special code inv_ get + * automatically passed to Invenio + * + * Other parameters: + *
    + *
  • q.op - the default operator "OR" or "AND"
  • + *
  • df - the default field name
  • + *
+ *
+ * Example: {!iq mode=maxinv xfields=fulltext}035:arxiv +bar -baz + * + * The example above would query everything as Invenio field, but fulltext will + * be served by Solr. + * + * Example: + * {!iq iq.mode=maxsolr iq.xfields=fulltext,abstract iq.channel=bitset}035:arxiv +bar -baz + * + * The example above will try to map all the fields into the Solr schema, if the + * field exists, it will be served by Solr. The fulltext will be served by + * Invenio no matter if it is defined in schema. And communication between Java + * and Invenio is done using bitset + * + * If the query is written as:inv_field:value the search will be + * always passed to Invenio. + * + */ +public class InvenioQParserPlugin extends QParserPlugin { + public static String NAME = "iq"; + public static String FIELDNAME = "InvenioQuery"; + public static String PREFIX = "inv_"; + public static String IDFIELD = "id"; + + @Override + public void init(NamedList args) { + } + + @Override + public QParser createParser(String qstr, SolrParams localParams, + SolrParams params, SolrQueryRequest req) { + return new InvenioQParser(qstr, localParams, params, req); + } + +} + +class InvenioQParser extends QParser { + + public static final Logger log = LoggerFactory + .getLogger(InvenioQParser.class); + + public static Pattern fieldPattern = Pattern + .compile("\\b([a-zA-Z_0-9]+)\\:"); + + String sortStr; + SolrQueryParser lparser; + ArrayList xfields = null; + + private String operationMode = "maxinvenio"; + private String exchangeType = "ints"; + private String querySyntax = "invenio"; + private IndexSchema schema = null; + + public InvenioQParser(String qstr, SolrParams localParams, + SolrParams params, SolrQueryRequest req) { + super(qstr, localParams, params, req); + + SolrParams solrParams = localParams == null ? params : new DefaultSolrParams(localParams, params); + + schema = req.getSchema(); + + String m = solrParams.get("iq.mode"); + if (m != null ) { + if (m.contains("maxinv") && !schema.hasExplicitField("*")) { + throw new SolrException( + null, + "Query parser is configured to pass as many fields to Invenio as possible, for this to work, schema must contain a dynamic field declared as '*'"); + } + operationMode = m; + } + + + xfields = new ArrayList(); + String[] overriden_fields = solrParams.getParams("iq.xfields"); + if (overriden_fields != null) { + for (String f: overriden_fields) { + if (f.indexOf(",") > -1) { + for (String x: f.split(",")) { + xfields.add(x); + } + } + else { + xfields.add(f); + } + } + } + + String eType = solrParams.get("iq.channel", "default"); + if (eType.contains("bitset")) { + exchangeType = "bitset"; + } + + String sType = solrParams.get("iq.syntax", "invenio"); + if (sType.contains("lucene")) { + querySyntax = "lucene"; + } + } + + public Query parse() throws ParseException { + + if (getString() == null) { + throw new ParseException("The query parameter is empty"); + } + + setString(normalizeInvenioQuery(getString())); + String qstr = getString(); + + + + // detect field not in the schema, but only if we are not in the + // all-tracking mode (because in that mode we can do it much smarter and + // without regex) + if (operationMode.equals("maxsolr") && !schema.hasExplicitField("*")) { + String q2 = changeInvenioQuery(req, qstr); + if (!q2.equals(qstr)) { + log.info(qstr + " --> " + q2); + setString(q2); + } + } + + + String defaultField = getParam(CommonParams.DF); + if (defaultField == null) { + defaultField = getReq().getSchema().getDefaultSearchFieldName(); + } + + + // Now use the specific parser to fight with the syntax + Query mainq; + + if (querySyntax.equals("invenio")) { + InvenioQueryParser invParser = new InvenioQueryParser(Version.LUCENE_29, schema.getDefaultSearchFieldName(), schema.getAnalyzer()); + String opParam = getParam(QueryParsing.OP); + if (opParam != null) { + invParser.setDefaultOperator("AND".equals(opParam) ? InvenioQueryParser.Operator.AND + : InvenioQueryParser.Operator.OR); + } else { + // try to get default operator from schema + QueryParser.Operator operator = getReq().getSchema() + .getSolrQueryParser(null).getDefaultOperator(); + invParser.setDefaultOperator(null == operator ? InvenioQueryParser.Operator.OR + : (operator == QueryParser.AND_OPERATOR ? InvenioQueryParser.Operator.AND : InvenioQueryParser.Operator.OR)); + } + mainq = invParser.parse(getString()); + } + else { + + lparser = new SolrQueryParser(this, defaultField); + // these could either be checked & set here, or in the SolrQueryParser + // constructor + String opParam = getParam(QueryParsing.OP); + if (opParam != null) { + lparser.setDefaultOperator("AND".equals(opParam) ? QueryParser.Operator.AND + : QueryParser.Operator.OR); + } else { + // try to get default operator from schema + QueryParser.Operator operator = getReq().getSchema() + .getSolrQueryParser(null).getDefaultOperator(); + lparser.setDefaultOperator(null == operator ? QueryParser.Operator.OR + : operator); + } + mainq = lparser.parse(getString()); + } + /** + else { + StandardQueryParser qpHelper = new StandardQueryParser(); + qpHelper.setAllowLeadingWildcard(true); + qpHelper.setAnalyzer(new StandardAnalyzer()); + try { + mainq = qpHelper.parse(getString(), schema.getDefaultSearchFieldName()); + } catch (QueryNodeException e) { + throw new ParseException(); + } + } + **/ + + Query mainq2; + try { + mainq2 = rewriteQuery(mainq, 0); + if (!mainq2.equals(mainq)) { + log.info(getString() + " --> " + mainq2.toString()); + mainq = mainq2; + } + } catch (IOException e) { + throw new ParseException(); + } + + return mainq; + } + + private Pattern weird_or = Pattern.compile("( \\|)([a-zA-Z\"])"); + private String normalizeInvenioQuery(String q) { + try { + Matcher matcher = weird_or.matcher(q); + q = matcher.replaceAll(" || $2"); + } + catch (Exception e) { + System.out.println(q); + } + q = q.replace("refersto:", "refersto\\:"); + q = q.replace("citedby:", "citedby\\:"); + q = q.replace("cited:", "cited\\:"); + q = q.replace("cocitedwith:", "cocitedwith\\:"); + q = q.replace("reportnumber:", "reportnumber\\:"); + q = q.replace("reference:", "reference\\:"); + return q; + + } + + /** + * Help method to change query into invenio fields (if the field is not defined + * in the schema, it is considered to be Invenio). However we use this simplistic + * rewriting only when '*' is not activated and when iq.mode=maxsolr + * @param req + * @param q + * @return + */ + private String changeInvenioQuery(SolrQueryRequest req, String q) { + IndexSchema schema = req.getSchema(); + // SolrQueryParser qparser = new SolrQueryParser(schema, "all"); + // log.info(qparser.escape(q)); + + // leave this to invenio + // q = q.replace("refersto:", "{!relation rel=refersto}"); + // q = q.replace("citedby:", "{!relation rel=citedby}"); + q = q.replace("journal:", "publication:"); + q = q.replace("arXiv:", "reportnumber:"); + + String q2 = q; + + Matcher matcher = fieldPattern.matcher(q); + while (matcher.find()) { + String field = q.substring(matcher.start(), matcher.end() - 1); + try { + if (schema.getFieldType(field) != null) { + continue; + } + } catch (SolrException e) { + // pass - not serious + } + q2 = q2.replace(field + ":", InvenioQParserPlugin.PREFIX + field + + ":"); + } + return q2; + } + + /** + * Returns a field (string) IFF we should pass the query to Invenio. + * + * @param field + * @return + * @throws ParseException + */ + private String getInvField(String field) throws ParseException { + String v = null; + // always consider it as Invenio field if the prefix is present + if (field.startsWith(InvenioQParserPlugin.PREFIX)) { + v = field.substring(InvenioQParserPlugin.PREFIX.length()); + return v; + } + + + // consider it as solr field if it is in the schema + if (operationMode.equals("maxsolr")) { + if(schema.hasExplicitField(field) && xfields.indexOf(field) == -1) { + return null; + } + return field; // consider it Invenio field + } + else { // pass all fields to Invenio + if (xfields.indexOf(field) > -1) { // besides explicitly solr fields + if (!schema.hasExplicitField(field)) { + throw new ParseException("The field '" + field + "' is not defined for Solr."); + } + return null; + } + return field; + } + + } + + + private Query createInvenioQuery(String field, String value, Map recidToDocid) { + Query newQuery = null; + String newField = field; + if (field.equals(schema.getDefaultSearchFieldName())) { + newField = ""; + } + if (exchangeType.equals("bitset")) { + newQuery = new InvenioQueryBitSet(new TermQuery(new Term(newField, value)), req, localParams, recidToDocid); + } + else { + newQuery = new InvenioQuery(new TermQuery(new Term(newField, value)), req, localParams, recidToDocid); + } + return newQuery; + + } + + + /** @see #QueryParsing.toString(Query,IndexSchema) */ + public Query rewriteQuery(Query query, int flags) throws IOException, + ParseException { + + boolean writeBoost = true; + + Query newQuery = null; + + SolrIndexReader reader = req.getSearcher().getReader(); + Map recidToDocid = null; + try { + recidToDocid = DictionaryCache.INSTANCE.getTranslationCache(reader, + InvenioQParserPlugin.IDFIELD); + } catch (IOException e) { + e.printStackTrace(); + throw new ParseException( + "Invenio translation table recid<->docid is not available!"); + } + + StringBuffer out = new StringBuffer(); + if (query instanceof TermQuery) { + TermQuery q = (TermQuery) query; + Term t = q.getTerm(); + String invf = getInvField(t.field()); + if (invf != null) { + newQuery = createInvenioQuery(invf, t.text(), recidToDocid); + } + + } else if (query instanceof TermRangeQuery) { + TermRangeQuery q = (TermRangeQuery) query; + String invf = getInvField(q.getField()); + if (invf != null) { + String fname = q.getField(); + FieldType ft = QueryParsing.writeFieldName(invf, schema, out, + flags); + out.append(q.includesLower() ? '[' : '{'); + String lt = q.getLowerTerm(); + String ut = q.getUpperTerm(); + if (lt == null) { + out.append('*'); + } else { + QueryParsing.writeFieldVal(lt, ft, out, flags); + } + + out.append(" TO "); + + if (ut == null) { + out.append('*'); + } else { + QueryParsing.writeFieldVal(ut, ft, out, flags); + } + + out.append(q.includesUpper() ? ']' : '}'); + + // newQuery = new + // TermRangeQuery(q.getField().replaceFirst(PREFIX, ""), + // q.getLowerTerm(), q.getUpperTerm(), + // q.includesLower(), q.includesUpper()); + newQuery = createInvenioQuery(invf, out.toString(), recidToDocid); + } + + } else if (query instanceof NumericRangeQuery) { + NumericRangeQuery q = (NumericRangeQuery) query; + String invf = getInvField(q.getField()); + if (invf != null) { + String fname = q.getField(); + FieldType ft = QueryParsing.writeFieldName(invf, schema, out, + flags); + out.append(q.includesMin() ? '[' : '{'); + Number lt = q.getMin(); + Number ut = q.getMax(); + if (lt == null) { + out.append('*'); + } else { + out.append(lt.toString()); + } + + out.append(" TO "); + + if (ut == null) { + out.append('*'); + } else { + out.append(ut.toString()); + } + + out.append(q.includesMax() ? ']' : '}'); + newQuery = createInvenioQuery(invf, out.toString(), recidToDocid); + + + // TODO: Invneio is using int ranges only, i think, but we shall + // not hardcode it here + // SchemaField ff = + // schema.getField(q.getField().substring(PREFIX.length())); + // newQuery = NumericRangeQuery.newIntRange(ff.getName(), + // (Integer)q.getMin(), (Integer)q.getMax(), + // q.includesMin(), q.includesMax()); + } + + } else if (query instanceof BooleanQuery) { + BooleanQuery q = (BooleanQuery) query; + newQuery = new BooleanQuery(); + + Listclauses = (List) q.clauses(); + + Query subQuery; + for (int i=0;i 0 ? "'" : "\""); + for (int i=0;i 0 ? "'" : "\""); + newQuery = createInvenioQuery(invf, out.toString(), recidToDocid); //TODO: is this correct? + } + + } else if (query instanceof FuzzyQuery) { + // do nothing + } else if (query instanceof ConstantScoreQuery) { + // do nothing + } else { + // do nothing + } + + if (newQuery != null) { + if (writeBoost && query.getBoost() != 1.0f) { + newQuery.setBoost(query.getBoost()); + } + return newQuery; + } else { + return query; + } + + } + + public String[] getDefaultHighlightFields() { + return new String[] { lparser.getField() }; + } + +} diff --git a/src/java/org/apache/solr/search/InvenioQuery.java b/src/java/org/apache/solr/search/InvenioQuery.java new file mode 100644 index 000000000..ea58fadec --- /dev/null +++ b/src/java/org/apache/solr/search/InvenioQuery.java @@ -0,0 +1,182 @@ +package org.apache.solr.search; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.ToStringUtils; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.search.InvenioQParserPlugin; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InvenioQuery extends Query { + + public static final Logger log = LoggerFactory + .getLogger(InvenioQuery.class); + + private float boost = 1.0f; // query boost factor + Query query; + SolrParams localParams; + SolrQueryRequest req; + Map recidToDocid = null; + + public InvenioQuery(TermQuery query, SolrQueryRequest req, + SolrParams localParams, Map recidToDocid) { + this.query = query; + this.localParams = localParams; + this.req = req; + this.recidToDocid = recidToDocid; + + } + + /** + * Sets the boost for this query clause to b. Documents + * matching this clause will (in addition to the normal weightings) have + * their score multiplied by b. + */ + public void setBoost(float b) { + query.setBoost(b); + } + + /** + * Gets the boost for this clause. Documents matching this clause will (in + * addition to the normal weightings) have their score multiplied by + * b. The boost is 1.0 by default. + */ + public float getBoost() { + return query.getBoost(); + } + + /** + * Expert: Constructs an appropriate Weight implementation for this query. + * + *

+ * Only implemented by primitive queries, which re-write to themselves. + */ + public Weight createWeight(Searcher searcher) throws IOException { + + return new InvenioWeight(this, localParams, req, recidToDocid); + } + + /** + * Expert: Constructs and initializes a Weight for a top-level query. + */ + public Weight weight(Searcher searcher) throws IOException { + Query query = searcher.rewrite(this); + Weight weight = query.createWeight(searcher); + float sum = weight.sumOfSquaredWeights(); + float norm = getSimilarity(searcher).queryNorm(sum); + if (Float.isInfinite(norm) || Float.isNaN(norm)) + norm = 1.0f; + weight.normalize(norm); + return weight; + } + + /** + * Expert: called to re-write queries into primitive queries. For example, a + * PrefixQuery will be rewritten into a BooleanQuery that consists of + * TermQuerys. + */ + public Query rewrite(IndexReader reader) throws IOException { + Query rewritten = query.rewrite(reader); + if (rewritten != query) { + InvenioQuery clone = (InvenioQuery) this.clone(); + clone.query = rewritten; + return clone; + } else { + return this; + } + } + + /** + * Expert: called when re-writing queries under MultiSearcher. + * + * Create a single query suitable for use by all subsearchers (in 1-1 + * correspondence with queries). This is an optimization of the OR of all + * queries. We handle the common optimization cases of equal queries and + * overlapping clauses of boolean OR queries (as generated by + * MultiTermQuery.rewrite()). Be careful overriding this method as + * queries[0] determines which method will be called and is not necessarily + * of the same type as the other queries. + */ + public Query combine(Query[] queries) { + return query.combine(queries); + + } + + /** + * Expert: adds all terms occurring in this query to the terms set. Only + * works if this query is in its {@link #rewrite rewritten} form. + * + * @throws UnsupportedOperationException + * if this query is not yet rewritten + */ + public void extractTerms(Set terms) { + query.extractTerms(terms); + } + + /** + * Expert: Returns the Similarity implementation to be used for this query. + * Subclasses may override this method to specify their own Similarity + * implementation, perhaps one that delegates through that of the Searcher. + * By default the Searcher's Similarity implementation is returned. + */ + public Similarity getSimilarity(Searcher searcher) { + return searcher.getSimilarity(); + } + + /** Prints a user-readable version of this query. */ + public String toString(String s) { + StringBuffer buffer = new StringBuffer(); + buffer.append("<"); + Term t = ((TermQuery) query).getTerm(); + if (t.field().length() > 0 ) { + buffer.append(t.field()); + buffer.append("|"); + } + buffer.append(t.text()); + //buffer.append(query.toString(s)); + buffer.append(">"); + buffer.append(ToStringUtils.boost(getBoost())); + return buffer.toString(); + } + + /** Returns true iff o is equal to this. */ + public boolean equals(Object o) { + if (o instanceof InvenioQuery) { + InvenioQuery fq = (InvenioQuery) o; + return (query.equals(fq.query) && getBoost() == fq.getBoost()); + } + return false; + } + + /** Returns a hash code value for this object. */ + public int hashCode() { + return query.hashCode() ^ Float.floatToRawIntBits(getBoost()); + } + + public String getInvenioQuery() { + String qfield = ((TermQuery) query).getTerm().field(); + String qval = ((TermQuery) query).getTerm().text(); + if (qfield.length() > 0) { + qval = qfield + ":" + qval; + } + if (qval.substring(0, 1).equals("\"/")) { + qval = qval.substring(1, qval.length()-1); + } + return qval; + } +} + diff --git a/src/java/org/apache/solr/search/InvenioQueryBitSet.java b/src/java/org/apache/solr/search/InvenioQueryBitSet.java new file mode 100644 index 000000000..8a2b9c749 --- /dev/null +++ b/src/java/org/apache/solr/search/InvenioQueryBitSet.java @@ -0,0 +1,38 @@ +package org.apache.solr.search; + +import java.io.IOException; +import java.util.Map; + +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.Weight; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.request.SolrQueryRequest; + + +public class InvenioQueryBitSet extends InvenioQuery { + + private static final long serialVersionUID = -2624111746562481355L; + private float boost = 1.0f; // query boost factor + + public InvenioQueryBitSet(TermQuery query, SolrQueryRequest req, + SolrParams localParams, Map recidToDocid) { + super(query, req, localParams, recidToDocid); + + } + + /** + * Expert: Constructs an appropriate Weight implementation for this query. + * + *

+ * Only implemented by primitive queries, which re-write to themselves. + */ + public Weight createWeight(Searcher searcher) throws IOException { + + return new InvenioWeightBitSet(this, localParams, req, recidToDocid); + } + + +} + + diff --git a/src/java/org/apache/solr/search/InvenioWeight.java b/src/java/org/apache/solr/search/InvenioWeight.java new file mode 100644 index 000000000..3bae510bb --- /dev/null +++ b/src/java/org/apache/solr/search/InvenioWeight.java @@ -0,0 +1,178 @@ +package org.apache.solr.search; + +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.io.IOException; +import java.util.Map; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.Weight; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.handler.InvenioHandler; +import org.apache.solr.request.SolrQueryRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class InvenioWeight extends Weight { + + public static final Logger log = LoggerFactory + .getLogger(InvenioWeight.class); + + protected Weight weight; + protected Similarity similarity; + protected InvenioQuery query; + protected TermQuery innerQuery; + protected SolrParams localParams; + protected Map recidToDocid; + protected float value; + + private int searcherCounter; + + public InvenioWeight(InvenioQuery query, SolrParams localParams, + SolrQueryRequest req, Map recidToDocid) + throws IOException { + SolrIndexSearcher searcher = req.getSearcher(); + this.innerQuery = (TermQuery) query.query; + this.weight = innerQuery.createWeight(searcher); + this.similarity = innerQuery.getSimilarity(searcher); + this.query = query; + this.localParams = localParams; + this.recidToDocid = recidToDocid; + this.searcherCounter = 0; + } + public Scorer scorer(IndexReader indexReader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + + if (searcherCounter > 0) { + return null; + } + searcherCounter++; + + // we override the Scorer for the InvenioQuery + return new Scorer(similarity) { + + private int doc = -1; + private int[] recids = null; + private int recids_counter = -1; + private int max_counter = -1; + + public void score(Collector collector) throws IOException { + collector.setScorer(this); + + int d; + while ((d = nextDoc()) != NO_MORE_DOCS) { + collector.collect(d); + } + } + + private void searchInvenio() throws IOException { + // ask Invenio to give us recids + String qval = query.getInvenioQuery(); + + PythonMessage message = MontySolrVM.INSTANCE + .createMessage("perform_request_search_ints") + .setSender("InvenioQuery").setParam("query", qval); + try { + MontySolrVM.INSTANCE.sendMessage(message); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new IOException("Error searching Invenio!"); + } + + Object result = message.getResults(); + if (result != null) { + recids = (int[]) result; + max_counter = recids.length - 1; + log.info("Invenio returned: " + recids.length + " hits"); + } + else { + log.info("Invenio returned: null"); + } + } + + public int nextDoc() throws IOException { + // this is called only once + if (this.doc == -1) { + searchInvenio(); + if (recids == null || recids.length == 0) { + return doc = NO_MORE_DOCS; + } + } + + recids_counter += 1; + if (recids_counter > max_counter) { + return doc = NO_MORE_DOCS; + } + + try { + doc = recidToDocid.get(recids[recids_counter]); + } + catch (NullPointerException e) { + log.error("Doc with recid=" + recids[recids_counter] + " missing. You should update Invenio recids!"); + throw e; + } + + return doc; + } + + public int docID() { + return doc; + } + + public int advance(int target) throws IOException { + while ((doc = nextDoc()) < target) { + } + return doc; + } + + public float score() throws IOException { + assert doc != -1; + return innerQuery.getBoost() * 1.0f; // TODO: implementation of the + // scoring algorithm + } + };// Scorer + }// scorer + + // pass these methods through to enclosed query's weight + public float getValue() { + return value; + } + + public float sumOfSquaredWeights() throws IOException { + return weight.sumOfSquaredWeights() * innerQuery.getBoost() + * innerQuery.getBoost(); + } + + public void normalize(float v) { + weight.normalize(v); + value = weight.getValue() * innerQuery.getBoost(); + } + + public Explanation explain(IndexReader ir, int i) throws IOException { + Explanation inner = weight.explain(ir, i); + if (innerQuery.getBoost() != 1) { + Explanation preBoost = inner; + inner = new Explanation(inner.getValue() * innerQuery.getBoost(), + "product of:"); + inner.addDetail(new Explanation(innerQuery.getBoost(), "boost")); + inner.addDetail(preBoost); + } + inner.addDetail(new Explanation(0.0f, "TODO: add formula details")); + return inner; + } + + // return this query + public Query getQuery() { + return query; + } + +}; // Weight + diff --git a/src/java/org/apache/solr/search/InvenioWeightBitSet.java b/src/java/org/apache/solr/search/InvenioWeightBitSet.java new file mode 100644 index 000000000..8006822a4 --- /dev/null +++ b/src/java/org/apache/solr/search/InvenioWeightBitSet.java @@ -0,0 +1,124 @@ +package org.apache.solr.search; + +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.ads.solr.InvenioBitSet; +import org.apache.commons.io.IOUtils; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.TermQuery; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.request.SolrQueryRequest; + + +import com.jcraft.jzlib.ZInputStream; + +public class InvenioWeightBitSet extends InvenioWeight { + + public InvenioWeightBitSet(InvenioQuery query, SolrParams localParams, + SolrQueryRequest req, Map recidToDocid) + throws IOException { + super(query, localParams, req, recidToDocid); + + } + + public Scorer scorer(IndexReader indexReader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + + // we override the Scorer for the InvenioQuery + return new Scorer(similarity) { + + private int doc = -1; + private int recid = -1; + private InvenioBitSet bitSet = null; + + public void score(Collector collector) throws IOException { + collector.setScorer(this); + + int d; + while ((d = nextDoc()) != NO_MORE_DOCS) { + collector.collect(d); + } + } + + private void searchInvenio() throws IOException { + // ask Invenio to give us recids + String qval = query.getInvenioQuery(); + + PythonMessage message = MontySolrVM.INSTANCE + .createMessage("perform_request_search_bitset") + .setSender("InvenioQuery").setParam("query", qval); + try { + MontySolrVM.INSTANCE.sendMessage(message); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new IOException("Error searching Invenio!"); + } + + Object result = message.getResults(); + + if (result != null) { + // use zlib to read in the data + InputStream is = new ByteArrayInputStream((byte[]) result); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + ZInputStream zIn = new ZInputStream(is); + + int bytesCopied = IOUtils.copy(zIn, bOut); + byte[] bitset_bytes = bOut.toByteArray(); + bitSet = new InvenioBitSet(bitset_bytes); + } + } + + public int nextDoc() throws IOException { + // this is called only once + if (this.recid == -1) { + searchInvenio(); + if (bitSet == null || bitSet.isEmpty()) { + return doc = NO_MORE_DOCS; + } + } + + if ((recid = bitSet.nextSetBit(recid)) == -1) { + return doc = NO_MORE_DOCS; + } + + try { + doc = recidToDocid.get(recid); + } + catch (NullPointerException e) { + log.error("Doc with recid=" + recid + " missing. You should update Invenio recids!"); + throw e; + } + + return doc; + } + + public int docID() { + return doc; + } + + public int advance(int target) throws IOException { + while ((doc = nextDoc()) < target) { + } + return doc; + } + + public float score() throws IOException { + assert doc != -1; + return query.getBoost() * 1.0f; // TODO: implementation of the + // scoring algorithm + } + };// Scorer + }// scorer + +}; // Weight + diff --git a/src/java/org/apache/solr/update/InvenioKeepRecidUpdated.java b/src/java/org/apache/solr/update/InvenioKeepRecidUpdated.java new file mode 100644 index 000000000..91085e876 --- /dev/null +++ b/src/java/org/apache/solr/update/InvenioKeepRecidUpdated.java @@ -0,0 +1,166 @@ +package org.apache.solr.update; + +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import invenio.montysolr.jni.PythonMessage; +import invenio.montysolr.jni.MontySolrVM; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.apache.lucene.document.Field; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.core.SolrCore; +import org.apache.solr.handler.RequestHandlerBase; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.request.SolrQueryResponse; +import org.apache.solr.schema.IndexSchema; +import org.apache.solr.util.DictionaryCache; + + + +/** + * Ping solr core + * + * @since solr 1.3 + */ +public class InvenioKeepRecidUpdated extends RequestHandlerBase +{ + @Override + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception + { + SolrParams params = req.getParams(); + SolrParams required = params.required(); + SolrCore core = req.getCore(); + IndexSchema schema = req.getSchema(); + + UpdateHandler updateHandler = core.getUpdateHandler(); + + + long start = System.currentTimeMillis(); + + AddUpdateCommand addCmd = new AddUpdateCommand(); + addCmd.allowDups = false; + addCmd.overwriteCommitted = false; + addCmd.overwritePending = false; + + + + int last_recid = -1; // -1 means get the first created doc + + if (params.getInt("last_recid") != null) { + last_recid = params.getInt("last_recid"); + } + else { + int[] ids = DictionaryCache.INSTANCE.getLuceneCache(req.getSearcher().getReader(), schema.getUniqueKeyField().getName()); + for(int m: ids) { + if (m > last_recid) { + last_recid = m; + } + } + } + + rsp.add("last_recid", last_recid); + + + Map dictData; + + if (params.getBool("generate", false)) { + Integer max_recid = params.getInt("max_recid", 0); + if (max_recid == 0 || max_recid < last_recid) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "The max_recid parameter missing!"); + } + + dictData = new HashMap(); + int[] a = new int[max_recid-last_recid]; + for (int i=0, ii=last_recid+1;ii) results; + } + + + if (dictData.containsKey("ADDED")) { + int[] recids = dictData.get("ADDED"); + // create new documentns, they will have only recids, but that's OK (for some + // people), sigh... + if (recids.length > 0) { + SolrInputDocument doc = null; + for (int i=0; i> cache = null; + + private HashMap> + cache = new HashMap>(4); + + private HashMap> + translation_cache = new HashMap>(2); + private HashMap + translation_cache_tracker = new HashMap(2); + + public void setCache(String name, Map value) { + cache.put(name, value); + } + + public Map getCache(String name) { + return cache.get(name); + } + + public int[] getLuceneCache(IndexReader reader, String field) throws IOException { + return FieldCache.DEFAULT.getInts(reader, field); + } + + public Map buildCache(int[] idMapping) throws IOException { + + Map fromFieldToLuceneId = new HashMap(idMapping.length); + int i = 0; + for (int value: idMapping) { + fromFieldToLuceneId.put(value, i); + i++; + } + return fromFieldToLuceneId; + } + + public Map getTranslationCache(IndexReader reader, String field) throws IOException { + int[] idMapping = getLuceneCache(reader, field); + Integer h = idMapping.hashCode(); + Integer old_hash = null; + if (translation_cache_tracker.containsKey(field)) + old_hash = translation_cache_tracker.get(field); + if (!h.equals(old_hash)) { + Map translTable = buildCache(idMapping); + translation_cache.put(field, translTable); + translation_cache_tracker.put(field, h); + } + return translation_cache.get(field); + } + +} diff --git a/src/java/org/apache/solr/util/WebUtils.java b/src/java/org/apache/solr/util/WebUtils.java new file mode 100644 index 000000000..20e8b2538 --- /dev/null +++ b/src/java/org/apache/solr/util/WebUtils.java @@ -0,0 +1,50 @@ +package org.apache.solr.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +public class WebUtils { + + public static Map> getUrlParameters(String url) + throws UnsupportedEncodingException { + Map> params = new HashMap>(); + String[] urlParts = url.split("\\?"); + if (urlParts.length > 1) { + String query = urlParts[1]; + for (String param : query.split("&")) { + String pair[] = param.split("="); + String key = URLDecoder.decode(pair[0], "UTF-8"); + String value = URLDecoder.decode(pair[1], "UTF-8"); + List values = params.get(key); + if (values == null) { + values = new ArrayList(); + params.put(key, values); + } + values.add(value); + } + } + return params; + } + + + public static Map parseQueryString(String encodedParams) + throws UnsupportedEncodingException { + final Map qps = new HashMap(); + final StringTokenizer pairs = new StringTokenizer(encodedParams, "&"); + while (pairs.hasMoreTokens()) { + final String pair = pairs.nextToken(); + final StringTokenizer parts = new StringTokenizer(pair, "="); + final String key = URLDecoder.decode(parts.nextToken(), "UTF-8"); + final String value = parts.hasMoreTokens() ? URLDecoder.decode(parts.nextToken(), "UTF-8") : ""; + + qps.put(key, value); + } + return qps; + } + +} diff --git a/src/python/montysolr/__init__.py b/src/python/montysolr/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/python/montysolr/examples/__init__.py b/src/python/montysolr/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/python/montysolr/examples/bigtest.py b/src/python/montysolr/examples/bigtest.py new file mode 100644 index 000000000..7956015ee --- /dev/null +++ b/src/python/montysolr/examples/bigtest.py @@ -0,0 +1,79 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +from montysolr.initvm import montysolr_java as sj +from montysolr.utils import MontySolrTarget + + +import random +import time + +def bigtest(message): + req = message.getSolrQueryRequest() + rsp = message.getSolrQueryResponse() + + params = req.getParams() + action = params.get("action") + + start = time.time() + + if 'recids' in action: + + size = params.getInt("size", 5000) + + if action == 'recids_int': + result = range(0, size) + result = sj.JArray_int(result) + elif action == 'recids_str': + result = [ '%s' % x for x in xrange(size)] + result = sj.JArray_string(result) + elif action == 'recids_hm_strstr': + result = sj.HashMap().of_(sj.String, sj.String) + for x in xrange(size): + result.put(str(x), str(x)) + elif action == 'recids_hm_strint': + result = sj.HashMap().of_(sj.String, sj.Integer) + for x in xrange(size): + result.put(str(x), x) + elif action == 'recids_hm_intint': + result = sj.HashMap().of_(sj.Integer, sj.Integer) + for x in xrange(size): + result.put(x, x) + elif action == 'recids_bitset': + from invenio import intbitset + filled = int(params.getInt('filled').intValue()) + result = intbitset.intbitset(rhs=size) + step = int(size / filled) + for x in xrange(0, size, step): + result.add(x) + result = sj.JArray_byte(result.fastdump()) + else: + result = None + + message.setResults(result) + + else: + help = ''' + action:args:description + recids_int:@size(int):returns array of integers of the given size + recids_str:@size(int):returns array of strings of the given size + recids_hm_strstr:@size(int):returns hashmap of string:string of the given size + recids_hm_strint:@size(int):returns hashmap of string:int of the given size + recids_hm_intint:@size(int):returns hashmap of int:int of the given size + recids_bitset:@size(int) - size of the bitset; @filled(int) - number of elements that are set:returns bit array (uses invenio bitset for the transfer) + ''' + for line in help.split('\n'): + rsp.add('python-message', line.strip()) + + + rsp.add('python-message', 'Python call finished in: %s ms.' % (time.time() - start)) + + +def montysolr_targets(): + targets = [ + MontySolrTarget(':bigtest', bigtest), + ] + return targets diff --git a/src/python/montysolr/examples/twitter_test.py b/src/python/montysolr/examples/twitter_test.py new file mode 100644 index 000000000..d1c09e60c --- /dev/null +++ b/src/python/montysolr/examples/twitter_test.py @@ -0,0 +1,62 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +from montysolr.initvm import montysolr_java as sj +from montysolr.utils import MontySolrTarget + +import twitter + +def twitter_api(message): + req = message.getSolrQueryRequest() + rsp = message.getSolrQueryResponse() + + params = req.getParams() + core = sj.SolrCore.cast_(req.getCore()) + schema = sj.IndexSchema.cast_(req.getSchema()) + updateHandler = sj.UpdateHandler.cast_(core.getUpdateHandler()) + + addCmd = sj.AddUpdateCommand() + addCmd.allowDups = False + addCmd.overwriteCommitted = False + addCmd.overwritePending = False + + + action = params.get("action") + + if action == 'search': + term = params.get("term") + + if not term: + rsp.add("python-message", 'Missing search term!') + return + api = twitter.Api() + docs = api.GetSearch(term) + for d in docs: + d = d.AsDict() + doc = sj.SolrInputDocument(); + doc.addField(schema.getUniqueKeyField().getName(), d['id']) + doc.addField("title", d['text']) + doc.addField("source", d['source']) + doc.addField("user", d['user']['screen_name']) + + addCmd.doc = sj.DocumentBuilder.toDocument(doc, schema) + updateHandler.addDoc(addCmd) + + updateCmd = sj.CommitUpdateCommand(True) # coz for demo we want to see it + updateHandler.commit(updateCmd) + + rsp.add('python-message', 'Found and indexed %s docs for term %s from Twitter' % (len(docs), term)) + + else: + rsp.add("python-message", 'Unknown action: %s' % action) + + + +def montysolr_targets(): + targets = [ + MontySolrTarget('TwitterAPIHandler:twitter_api', twitter_api), + ] + return targets diff --git a/src/python/montysolr/handler.py b/src/python/montysolr/handler.py new file mode 100644 index 000000000..a95c1805e --- /dev/null +++ b/src/python/montysolr/handler.py @@ -0,0 +1,182 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +import logging +import traceback +import sys +import imp +import os + +class Handler(object): + '''Handler objects are responsible for passing messages from the MontySolr + bridge towards the real method that knows what to do with them. Because + the handler is potentially expensive to create, they are always singletons. + + Of course, this is the basic class + ''' + def __init__(self): + self._db = {} + self.log = logging + self.init() + + def init(self): + raise NotImplemented("This method must be overriden") + + + def handle_message(self, message): + '''Receives the messages, finds the target of the message + and calls it, passing it the message instance''' + message.threadInfo("handle_message") + target = self.get_target(message) + if target: + target(message) + + + + def get_target(self, message): + """Must return only a callables that receive + a PythonMessage object""" + recipient = message.getReceiver() + sender = message.getSender() + + message_id = (sender or '') + ':' + recipient + if message_id in self._db: + return self._db[message_id] + else: + self.log.error("Unknown target; sender=%s, recipient=%s, message_id=%s" % + (sender, recipient, message_id)) + + def discover_targets(self, places): + '''Queries the different objects for existence of the callable + called montysolr_targets. If that callable is present, it will + get from it the MontySolrTarget instances, which represent the + message_id and target -- for the (PythonMessage) objects + @var places: (list) must be a list of either strings + example: + 'package.module' + package.module has a method 'montysolr_targets' + '/tmp/package/module/someothermodule.py' + we create a new anonymous module and call its + 'montysolr_targets' method + or the object may be a python objects that has a + callble method 'montysolr_targets' + ''' + if not isinstance(places, list): + raise Exception("The argument must be a list") + + for place in places: + if isinstance(place, basestring): + if os.path.exists(place): # it is a module + try: + obj = self.create_module(place) + self.retrieve_targets(obj) + except: + self.log.error(traceback.format_exc()) + else: + obj = self.import_module(place) + self.retrieve_targets(obj) + else: + self.retrieve_targets(place) + + def import_module(self, module_name): + """Import workflow module + @var workflow: string as python import, eg: merkur.workflow.load_x""" + mod = __import__(module_name) + components = module_name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + + def create_module(self, file, anonymous=False, fail_onerror=True): + """ Initializes module into a separate object (not included in sys) """ + name = 'MontySolrTmpModule<%s>' % os.path.basename(file) + x = imp.new_module(name) + x.__file__ = file + x.__id__ = name + x.__builtins__ = __builtins__ + + # XXX - chdir makes our life difficult, especially when + # one workflow wrap another wf and relative paths are used + # in the config. In such cases, the same relative path can + # point to different locations just because location of the + # workflow (parts) are different + # The reason why I was using chdir is because python had + # troubles to import files that containes non-ascii chars + # in their filenames. That is important for macros, but not + # here. + + # old_cwd = os.getcwd() + + try: + #filedir, filename = os.path.split(file) + #os.chdir(filedir) + if anonymous: + execfile(file, x.__dict__) + else: + #execfile(file, globals(), x.locals()) + exec open(file).read() + except Exception, excp: + if fail_onerror: + raise Exception(excp) + else: + self.log.error(excp) + self.log.error(traceback.format_exc()) + return + return x + + def retrieve_targets(self, obj): + if hasattr(obj, 'montysolr_targets'): + db = self._db + for t in obj.montysolr_targets(): + message_id = t.getMessageId() + target = t.getTarget() + if message_id in db: + raise Exception("The message with id '%s' already has a target:" % + (message_id, db[message_id])) + db[message_id] = target + else: + self.log.error("The %s has no method 'montysolr_targets'" % obj) + + if ':diagnostic_test' not in db: + db[':diagnostic_test'] = self._diagnostic_target() + + + def _diagnostic_target(self): + def diagnostic_target(message): + out = [] + out.append("PYTHONPATH: %s" % "\n ".join(sys.path)) + out.append("PYTHONHOME: %s" % os.getenv("PYTHONHOME")) + out.append("PATH: %s" % os.getenv("PATH")) + out.append("LD_LIBRARY_PATH: %s" % os.getenv("LD_LIBRARY_PATH")) + + out.append('---') + out.append('handler: %s' % self) + + + out.append('---') + out.append('current targets: %s' % " \n".join(map(lambda x: '%s --> %s' % x, self._db.items()))) + + out.append('---') + out.append('running diagnostic tests') + for k,v in self._db.items(): + if 'diagnostic_test' in k and k != ':diagnostic_test': + out.append('===================') + out.append(k) + try: + v(message) + res = message.getResults() + out.append(str(res)) + except: + out.append(traceback.format_exc()) + out.append('===================') + + message.setResults('\n'.join(out)) + + return diagnostic_target + + + + diff --git a/src/python/montysolr/initvm.py b/src/python/montysolr/initvm.py new file mode 100644 index 000000000..b3c09b302 --- /dev/null +++ b/src/python/montysolr/initvm.py @@ -0,0 +1,45 @@ +''' +Created on Jan 13, 2011 + +@author: rca +''' +import os +import sys + +import lucene + +try: + import solr_java + import montysolr_java +except: + _d = os.path.abspath(os.path.dirname(__file__) + '/../../build/dist') + if _d not in sys.path and os.path.exists(_d): + sys.stderr.write('Warning: we add the default folder to sys.path:\n') + sys.stderr.write(_d + '\n') + sys.path.append(_d) + import solr_java + import montysolr_java + + +if os.getenv('MONTYSOLR_DEBUG'): + from invenio import remote_debugger + remote_debugger.start('3') #or override '3|ip:192.168.31.1|port:9999' + + +_jvmargs = '' +if os.getenv('MONTYSOLR_JVMARGS_PYTHON'): + _jvmargs = os.getenv('MONTYSOLR_JVMARGS_PYTHON') + +# the distribution may contain a file that lists the jars that weere used +# for comilation, get the and add them to the classpath +_cp = os.path.join(os.path.dirname(montysolr_java.__file__), 'classpath') +_classpath='' +if os.path.exists(_cp): + _classpath = open(_cp, 'r').read() + +if _jvmargs: + montysolr_java.initVM(lucene.CLASSPATH+os.pathsep+montysolr_java.CLASSPATH+os.pathsep+_classpath, vmargs=_jvmargs) +else: + montysolr_java.initVM(lucene.CLASSPATH+os.pathsep+montysolr_java.CLASSPATH+os.pathsep+_classpath) +lucene.initVM() +solr_java.initVM() diff --git a/src/python/montysolr/inveniopie/__init__.py b/src/python/montysolr/inveniopie/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/python/montysolr/inveniopie/api_calls.py b/src/python/montysolr/inveniopie/api_calls.py new file mode 100644 index 000000000..e456a92ee --- /dev/null +++ b/src/python/montysolr/inveniopie/api_calls.py @@ -0,0 +1,114 @@ + + +import os +import thread + +from invenio import search_engine +from invenio import search_engine_summarizer +from invenio import dbquery +from invenio import bibrecord +from invenio.intbitset import intbitset + +from invenio.bibrank_citation_searcher import get_citation_dict + + + +from cStringIO import StringIO + +def dispatch(func_name, *args, **kwargs): + """Dispatches the call to the *local* worker + It returns a tuple (ThreadID, result) + """ + tid = thread.get_ident() + out = globals()[func_name](*args, **kwargs) + return [tid, out] + +def get_recids_changes(last_recid, max_recs=10000): + + search_op = '>' + + if last_recid == -1: + l = list(dbquery.run_sql("SELECT id FROM bibrec ORDER BY creation_date ASC LIMIT 1")) + search_op = '>=' + else: + # let's make sure we have a valid recid (or get the close valid one) + l = list(dbquery.run_sql("SELECT id FROM bibrec WHERE id >= %s LIMIT 1", (last_recid,))) + if not len(l): + return + last_recid = l[0][0] + + # there is not api to get this (at least i haven't found it) + mod_date = search_engine.get_modification_date(last_recid, fmt="%Y-%m-%d %H:%i:%S") + if not mod_date: + return + modified_records = list(dbquery.run_sql("SELECT id,modification_date, creation_date FROM bibrec " + "WHERE modification_date " + search_op + "%s LIMIT %s", (mod_date, max_recs ))) + + out = {'DELETED': [], 'CHANGED': [], 'ADDED': []} + for recid, mod_date, create_date in modified_records: + if mod_date == create_date: + out['ADDED'].append(recid) + else: + rec = search_engine.get_record(recid) + status = bibrecord.record_get_field_value(rec, tag='980', code='c') + if status == 'DELETED': + out['DELETED'].append(recid) + else: + out['CHANGED'].append(recid) + return out + +def citation_summary(recids, of, ln, p, f): + out = StringIO() + x = search_engine_summarizer.summarize_records(recids, of, ln, p, f, out) + if x: + output = x + else: + out.seek(0) + output = out.read() + return output + +def search(q, max_len=25): + offset = 0 + #hits = search_engine.search_pattern_parenthesised(None, q) + hits = search_engine.perform_request_search(None, p=q) + total_matches = len(hits) + + if max_len: + return [offset, hits[:max_len], total_matches] + else: + return [offset, hits, total_matches] + +def sort_and_format(hits, kwargs): + + kwargs = search_engine._cleanup_arguments(**kwargs) + t1 = os.times()[4] + req = StringIO() + kwargs['req'] = req + + if 'hosted_colls_actual_or_potential_results_p' not in kwargs: + kwargs['hosted_colls_actual_or_potential_results_p'] = True # this prevents display of the nearest-term box + + # search stage 4 and 5: intersection with collection universe and sorting/limiting + output = search_engine._collect_sort_display(hits, kwargs=kwargs, **kwargs) + if output is not None: + req.seek(0) + return req.read() + output + + t2 = os.times()[4] + cpu_time = t2 - t1 + kwargs['cpu_time'] = cpu_time + + recids = search_engine._rank_results(kwargs=kwargs, **kwargs) + + if 'of' in kwargs and kwargs['of'].startswith('hc'): + output = citation_summary(intbitset(recids), kwargs['of'], kwargs['ln'], kwargs['p'], kwargs['f']) + if output: + return output + + return recids + + + + +if __name__ == '__main__': + print dispatch("get_recids_changes", 85) diff --git a/src/python/montysolr/inveniopie/multiprocess_api_calls.py b/src/python/montysolr/inveniopie/multiprocess_api_calls.py new file mode 100644 index 000000000..bc8c925ff --- /dev/null +++ b/src/python/montysolr/inveniopie/multiprocess_api_calls.py @@ -0,0 +1,127 @@ +''' +Created on Feb 13, 2011 + +@author: rca +''' + +from montysolr.inveniopie import api_calls +from invenio.intbitset import intbitset +import os +import multiprocessing + + +POOL = None + +# ====================================================== +# Multiprocess versions of the api_call methods +# ====================================================== + +def citation_summary_local_pre(args, kwargs): + args[0] = args[0].fastdump() + +def citation_summary_remote_pre(args, kwargs): + args[0] = intbitset().fastload(args[0]) + +def search_remote_post_X(result): + if result: + res = result[1] + if len(res) > 0: + result[1] = intbitset(res).fastdump() + return result + +def search_local_post_X(result): + if result: + res = result[1] + if isinstance(res, basestring): + result[1] = intbitset().fastload(res).tolist() + return result + +def sort_and_format_local_pre(args, kwargs): + args[0] = intbitset(args[0]).fastdump() + +def sort_and_format_remote_pre(args, kwargs): + args[0] = intbitset().fastload(args[0]) + + +# ====================================================== +# Start of the multi-processing +# ====================================================== + +def start_multiprocessing(num_proc=None, default=4): + global POOL + if not num_proc: + try: + num_proc = multiprocessing.cpu_count() + except: + num_proc = default + POOL = multiprocessing.Pool(processes=num_proc) + else: + POOL = multiprocessing.Pool(processes=num_proc) + +# ====================================================== +# Some code to execute on lazy-initialization +# ====================================================== + +from invenio import bibrank_citation_searcher as bcs, \ + search_engine_summarizer as ses + +# initialize citation dictionaries in parent (so that forks have them shared) +bcs.get_citation_dict("citationdict") +bcs.get_citation_dict("reversedict") + + +# ====================================================== +# Dispatching code +# ====================================================== + +def dispatch(func_name, *args, **kwargs): + """Dispatches the call to the remote worker""" + g = globals() + func_name_pre = '%s_local_pre' % func_name + func_name_post = '%s_local_post' % func_name + + if func_name_pre in g: + args = list(args) + g[func_name_pre](args, kwargs) + + handle = POOL.apply_async(_dispatch_remote, args=(func_name, args, kwargs)) + (worker_pid, result) = handle.get() + + if func_name_post in g: + result = g[func_name_post](result) + + return(worker_pid, result) + + + +def _dispatch_remote(func_name, args, kwargs): + """This receives the data on the remote side and calls + the actual function that does the job and returns results. + """ + + g = globals() + func_name_pre = '%s_remote_pre' % func_name + func_name_post = '%s_remote_post' % func_name + + if func_name_pre in g: + args = list(args) + g[func_name_pre](args, kwargs) + + (thread_id, result) = api_calls.dispatch(func_name, *args, **kwargs) + + if func_name_post in g: + result = g[func_name_post](result) + + return (os.getpid(), result) + + +def _dispatch(func_name, *args, **kwargs): + return api_calls.dispatch(func_name, *args, **kwargs) + + + + + + + + diff --git a/src/python/montysolr/inveniopie/targets.py b/src/python/montysolr/inveniopie/targets.py new file mode 100644 index 000000000..ee1d744d1 --- /dev/null +++ b/src/python/montysolr/inveniopie/targets.py @@ -0,0 +1,314 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +from cStringIO import StringIO +from invenio.intbitset import intbitset +from montysolr.initvm import montysolr_java as sj +from montysolr.utils import MontySolrTarget +import logging +import os +import montysolr.inveniopie.multiprocess_api_calls as api_calls + +import time + + + +def format_search_results(message): + req = message.getSolrQueryRequest() + rsp = message.getSolrQueryResponse() + recids = message.getParamArray_int("recids") + start = time.time() + message.threadInfo("start: citation_summary") + c_time = time.time() + iset = intbitset(recids) + message.threadInfo("int[] converted to intbitset in: %s, size=%s" % (time.time() - c_time, len(iset))) + (wid, (output)) = api_calls.dispatch('citation_summary', iset, 'hcs', 'en', '', '') + message.threadInfo("end: citation_summary pid=%s, finished in %s" % (wid, time.time() - start)) + rsp.add("inv_response", output) + +def format_search_results_local(message): + req = message.getSolrQueryRequest() + rsp = message.getSolrQueryResponse() + + recids = message.getParamArray_int("recids") + out = StringIO() + # TODO: pass the ln and other arguments + (wid, (output,)) = api_calls.dispatch("sumarize_records", intbitset(recids), 'hcs', 'en', '', '', out) + if not output: + out.seek(0) + output = out.read() + del out + rsp.add("inv_response", output) + + + +def perform_request_search_bitset(message): + query = unicode(message.getParam("query")).encode("utf8") + #offset, hit_dump, total_matches, searcher_id = searching.multiprocess_search(query, 0) + (wid, (offset, hits, total_matches)) = api_calls.dispatch('search', query, 0) + #message.threadInfo("query=%s, total_hits=%s" % (query, total_matches)) + message.setResults(sj.JArray_byte(intbitset(hits).fastdump())) + +def perform_request_search_ints(message): + query = unicode(message.getParam("query")).encode("utf8") + #offset, hit_list, total_matches, searcher_id = searching.multiprocess_search(query, 0) + (wid, (offset, hits, total_matches)) = api_calls.dispatch('search', query, 0) + if len(hits): + message.setResults(sj.JArray_int(hits)) + else: + message.setResults(sj.JArray_int([])) + + message.setParam("total", total_matches) + +def handle_request_body(message): + req = message.getSolrQueryRequest() + rsp = message.getSolrQueryResponse() + params = message.getParams() + + start = time.time() + q = params.get("q").encode('utf8') #TODO: sj.CommonParams.Q is overshadowed by solr.util.CommonParams or is not wrapped at all + + #offset, hit_list, total_matches, searcher_id = searching.multiprocess_search(str(q)) + (wid, (offset, hit_list, total_matches)) = api_calls.dispatch('search', str(q)) + + t = time.time() - start + #message.threadInfo("Query took: %s s. hits=%s and was executed by: %s" % (t, total_matches, searcher_id)) + + reader = req.getSearcher().getReader(); + + # translate invenio recids into lucene docids + transl_table = sj.DictionaryCache.INSTANCE.getTranslationCache(reader, "id") + res = [] + for h in hit_list: + if transl_table.containsKey(h): + res.append(transl_table.get(h)) + + #logging.error(transl_table.size()) + + ds = sj.DocSlice(offset,len(res),res, None, total_matches, 1.0) + rsp.add("response", ds) + +def get_recids_changes(message): + """Retrieves the recids of the last changed documents""" + last_recid = int(sj.Integer.cast_(message.getParam("last_recid")).intValue()) + max_records = 10000 + if message.getParam('max_records'): + mr = int(sj.Integer.cast_(message.getParam("max_records")).intValue()) + if mr < 100001: + max_records = mr + (wid, results) = api_calls.dispatch("get_recids_changes", last_recid, max_records) + if results: + out = sj.HashMap().of_(sj.String, sj.JArray_int) + for k,v in results.items(): + out.put(k, sj.JArray_int(v)) + message.setResults(out) + + + + +def get_citation_dict(message): + + dictname = str(message.getParam('dictname')) + hm = sj.HashMap.cast_(message.getParam('result')) + + # we will call the local module (not dispatched remotely) + cd = api_calls._dispatch("get_citation_dict", dictname) + message.threadInfo("%s: %s" % (dictname, str(len(cd)))) + if cd: + #hm = sj.HashMap().of_(sj.Integer, sj.JArray_int) + + message.threadInfo('creating hashmap') + for k,v in cd.items(): + j_array = sj.JArray_int(v) + hm.put(int(k), j_array) + message.threadInfo('finished') + +def workout_field_value(message): + sender = str(message.getSender()) + if sender in 'PythonTextField': + value = message.getParam('externalVal') + if not value: + return + value = str(value) + #print 'searching for', value + vals = {} + #ret = value.lower() + ' Hey! ' + ret = None + if value: + parts = value.split('|') + for p in parts: + k, v = p.split(':', 1) + if v[0] == '[' and v[-1] == ']': + v = v[1:-1] + vals[k] = v + if 'arxiv_id' in vals and 'src_dir' in vals: + #print vals + dirs = vals['src_dir'].split(',') + ax = vals['arxiv_id'].split(',')[0].strip() + if ax.find('/') > -1: + arx_parts = ax.split('/') #math-gt/060889 + fname = ''.join(arx_parts) + topdir = arx_parts[1][:4] + elif ax.find('.') > -1: + arx_parts = ax.replace('arXiv:', '').split('.', 1) #arXiv:0712.0712 + topdir = arx_parts[0] + fname = '.'.join(arx_parts) + else: + return ret + + if len(arx_parts) == 2: + + + for d in dirs: + #print (d, topdir, fname + '.txt') + newname = os.path.join(d, topdir, fname + '.txt') + if os.path.exists(newname): + fo = open('/tmp/solr-index.txt', 'a') + fo.write(newname + '\n') + fo.close() + ret = open(newname, 'r').read() + if ret: + message.setResults(ret.decode('utf8')) + else: + fo = open('/tmp/solr-not-found.txt', 'a') + fo.write('%s\t%s\n' % (newname, value)) + fo.close() + break + + + +def sort_and_format(message): + req = message.getSolrQueryRequest() + rsp = message.getSolrQueryResponse() + + recids = intbitset(message.getParamArray_int("recids")) + kwargs = sj.HashMap.cast_(message.getParam('kwargs')) + + kws = {} + + kset = kwargs.keySet().toArray() + vset = kwargs.values().toArray() + max_size = len(vset) + i = 0 + while i < max_size: + v = str(vset[i]) + if v[0:1] in ["'", '[', '{'] : + try: + v = eval(v) + except: + pass + kws[str(kset[i])] = v + i += 1 + + start = time.time() + message.threadInfo("start: citation_summary") + c_time = time.time() + + message.threadInfo("int[] converted to intbitset in: %s, size=%s" % (time.time() - c_time, len(recids))) + (wid, (output)) = api_calls._dispatch('sort_and_format', recids, kws) + + message.threadInfo("end: citation_summary pid=%s, finished in %s" % (wid, time.time() - start)) + + if isinstance(output, list): + message.setResults(sj.JArray_int(output)) + message.setParam("rtype", "int") + else: + message.setResults(output) + message.setParam("rtype", "string") + +def diagnostic_test(message): + out = [] + message.setParam("query", "boson") + perform_request_search_ints(message) + res = sj.JArray_int.cast_(message.getResults()) + out.append('Search for "boson" retrieved: %s hits' % len(res) ) + out.append('Total hits: %s' % sj.Integer.cast_(message.getParam("total"))) + message.setResults('\n'.join(out)) + +''' +def _get_solr(): + # HACK: this should be lazy loaded and in a separate module + from montysolr.python_bridge import JVMBridge + if not hasattr(sj, '__server') and not JVMBridge.hasObj("solr.server"): + initializer = sj.CoreContainer.Initializer() + conf = {'solr_home': '/x/dev/workspace/sandbox/montysolr/example/solr', + 'data_dir': '/x/dev/workspace/sandbox/montysolr/example/solr/data'} + + sj.System.setProperty('solr.solr.home', conf['solr_home']) + sj.System.setProperty('solr.data.dir', conf['data_dir']) + core_container = initializer.initialize() + server = sj.EmbeddedSolrServer(core_container, "") + JVMBridge.setObj("solr.server", server) + JVMBridge.setObj("solr.container", core_container) + sj.__server = server + return server + return sj.__server + return JVMBridge.getObj("solr.server") + + +def search_unit_solr(message): + """Called from search_engine""" + from montysolr.python_bridge import JVMBridge + + sj = JVMBridge.getObjMontySolr() + server = _get_solr() + q = str(message.getParam("query")) #String + + query = sj.SolrQuery() + query.setQuery(q) + query.setParam("fl", ("id",)) + query_response = server.query(query) + + head_part = query_response.getResponseHeader() + res_part = query_response.getResults() + qtime = query_response.getQTime() + etime = query_response.getElapsedTime() + nf = res_part.getNumFound() + + a_size = res_part.size() + res = sj.JArray_int(a_size) + res_part = res_part.toArray() + if a_size: + #it = res_part.iterator() + #i = 0 + #while it.hasNext(): + for i in xrange(a_size): + #x = it.next() + doc = sj.SolrDocument.cast_(res_part[i]) + # we must do this gymnastics because of the tests + s = str(doc.getFieldValue("id")) # 002800500 + if s[0] == '0': + s = s[3:] # 800500 + res[i] = int(s) + #i += 1 + + message.setParam("QTime", qtime) + message.setParam("ElapsedTime", etime) + message.setResults(res) +''' + +def montysolr_targets(): + targets = [ + MontySolrTarget('PythonTextField:workout_field_value', workout_field_value), + MontySolrTarget('handleRequestBody', handle_request_body), + MontySolrTarget('rca.python.solr.handler.InvenioHandler:handleRequestBody', handle_request_body), + MontySolrTarget('CitationQuery:get_citation_dict', get_citation_dict), + MontySolrTarget('InvenioQuery:perform_request_search_ints', perform_request_search_ints), + MontySolrTarget('InvenioQuery:perform_request_search_bitset', perform_request_search_bitset), + MontySolrTarget('InvenioFormatter:format_search_results', format_search_results), + MontySolrTarget('InvenioKeepRecidUpdated:get_recids_changes', get_recids_changes), + MontySolrTarget('InvenioFormatter:sort_and_format', sort_and_format), + MontySolrTarget('Invenio:diagnostic_test', diagnostic_test), + ] + + + # start multiprocessing with that many processes in the pool + if hasattr(api_calls, "start_multiprocessing"): + if os.getenv('MONTYSOLR_MAX_WORKERS'): + api_calls.start_multiprocessing(int(os.getenv('MONTYSOLR_MAX_WORKERS'))) + else: + api_calls.start_multiprocessing() + return targets diff --git a/src/python/montysolr/java_bridge.py b/src/python/montysolr/java_bridge.py new file mode 100644 index 000000000..3e11e08b9 --- /dev/null +++ b/src/python/montysolr/java_bridge.py @@ -0,0 +1,37 @@ + +from montysolr.initvm import montysolr_java as sj + + + +''' +Created on Jan 13, 2011 + +@author: rca +''' + +DEBUG = False + +class SimpleBridge(sj.MontySolrBridge): + + def __init__(self, handler=None): + if not handler: + import montysolr.sequential_handler as handler_module + handler = handler_module.Handler + super(SimpleBridge, self).__init__() + self._handler = handler + self._handler_module = handler.__module__ + + def receive_message(self, message): + if DEBUG: + # HACK: to remove this whole block + req = message.getSolrQueryRequest() + if req: + params = req.getParams() + if params.get("reload"): + message.threadInfo('Reloading python!', self._handler_module) + self._handler_module = reload(self._handler_module) + self._handler = self._handler_module.Handler + self._handler.handle_message(message) + + def set_handler(self, handler): + self._handler = handler diff --git a/src/python/montysolr/python_bridge.py b/src/python/montysolr/python_bridge.py new file mode 100644 index 000000000..4179471a0 --- /dev/null +++ b/src/python/montysolr/python_bridge.py @@ -0,0 +1,76 @@ + +''' +Created on Feb 7, 2011 + +@author: rca + +This class serves the same purpose as its java counterpart +MontySolrVM - but it is a singleton that contains a reference +to the handlers - we don't need to instantiate it everytime +as is the case when calling from Java. Therefore we can +put the VM and the bridge parts together. + +It intentionally has the java-style method names +''' + +from montysolr import initvm +import sys + +class JVMBridge(object): + + def __new__(cls, *args): + if hasattr(initvm.montysolr_java, '_JVMBridge_SINGLETON'): + return getattr(initvm.montysolr_java, '_JVMBridge_SINGLETON') + else: + instance = super(JVMBridge, cls).__new__(cls) + setattr(initvm.montysolr_java, '_JVMBridge_SINGLETON', instance) + instance._store = {} + return instance + + def __del__(self): + #if 'solr.container' in self._store: + # self._store['solr.container'].shutdown() + sys.stderr.write('!!!!!!! - Bridge deleted') + + def __init__(self, handler=None): + + if not handler: #FIXME: we must make the handler configurable outside the code + import montysolr.sequential_handler as handler + handler = handler.Handler + self._handler = handler + self._lucene = initvm.lucene + self._solr = initvm.solr_java + self._sj = initvm.montysolr_java + + + def sendMessage(self, message): + self._sj.getVMEnv().attachCurrentThread() + self._handler.handle_message(message) + + def setHandler(self, handler): + self._handler = handler + + def createMessage(self, receiver): + self._sj.getVMEnv().attachCurrentThread() + return self._sj.PythonMessage(receiver) + + def getObjMontySolr(self): + return self._sj + + def getObjLucene(self): + return self._lucene + + def getObjSolr(self): + return self._solr + + def setObj(self, name, value): + self._store[name] = value + + def getObj(self, name): + return self._store[name] + + def hasObj(self, name): + return name in self._store + +JVMBridge = JVMBridge() + \ No newline at end of file diff --git a/src/python/montysolr/sequential_handler.py b/src/python/montysolr/sequential_handler.py new file mode 100644 index 000000000..423cc3f04 --- /dev/null +++ b/src/python/montysolr/sequential_handler.py @@ -0,0 +1,19 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +from montysolr import handler + + +class Handler(handler.Handler): + '''Simple handler that just calls the methods + ''' + + def init(self): + self.discover_targets(['montysolr.inveniopie.targets', 'montysolr.examples.twitter_test']) + + + +Handler = Handler() diff --git a/src/python/montysolr/tests/__init__.py b/src/python/montysolr/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/python/montysolr/tests/run_jetty_servlet.py b/src/python/montysolr/tests/run_jetty_servlet.py new file mode 100644 index 000000000..06fede44d --- /dev/null +++ b/src/python/montysolr/tests/run_jetty_servlet.py @@ -0,0 +1,33 @@ +''' +Created on Jan 10, 2011 + +@author: rca +''' + +import montysolr_java +import urllib2 + +def run(): + cp = '/x/dev/workspace/apache-solr-1.4.1/lib/commons-codec-1.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/commons-csv-1.0-SNAPSHOT-r609327.jar:/x/dev/workspace/apache-solr-1.4.1/lib/commons-fileupload-1.2.1.jar:/x/dev/workspace/apache-solr-1.4.1/lib/commons-httpclient-3.1.jar:/x/dev/workspace/apache-solr-1.4.1/lib/commons-io-1.4.jar:/x/dev/workspace/apache-solr-1.4.1/lib/easymock.jar:/x/dev/workspace/apache-solr-1.4.1/lib/geronimo-stax-api_1.0_spec-1.0.1.jar:/x/dev/workspace/apache-solr-1.4.1/lib/jcl-over-slf4j-1.5.5.jar:/x/dev/workspace/apache-solr-1.4.1/lib/junit-4.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-analyzers-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-core-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-highlighter-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-memory-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-misc-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-queries-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-snowball-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/lucene-spellchecker-2.9.3.jar:/x/dev/workspace/apache-solr-1.4.1/lib/servlet-api-2.4.jar:/x/dev/workspace/apache-solr-1.4.1/lib/slf4j-api-1.5.5.jar:/x/dev/workspace/apache-solr-1.4.1/lib/slf4j-jdk14-1.5.5.jar:/x/dev/workspace/apache-solr-1.4.1/lib/wstx-asl-3.2.7.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-cell-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-cell-docs-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-clustering-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-clustering-docs-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-core-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-core-docs-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-dataimporthandler-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-dataimporthandler-docs-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-dataimporthandler-extras-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-solrj-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-solrj-docs-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/apache-solr-velocity-docs-1.4.2-dev.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/commons-codec-1.3.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/commons-httpclient-3.1.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/commons-io-1.4.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/geronimo-stax-api_1.0_spec-1.0.1.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/jcl-over-slf4j-1.5.5.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/slf4j-api-1.5.5.jar:/x/dev/workspace/apache-solr-1.4.1/dist/solrj-lib/wstx-asl-3.2.7.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/jetty-6.1.3.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/jetty-util-6.1.3.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/jsp-2.1/ant-1.6.5.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/jsp-2.1/core-3.1.1.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/jsp-2.1/jsp-2.1.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/jsp-2.1/jsp-api-2.1.jar:/x/dev/workspace/apache-solr-1.4.1/example/lib/servlet-api-2.5-6.1.3.jar' + montysolr_java.initVM(montysolr_java.CLASSPATH + ':' + cp) + montysolr_java.System.setProperty('solr.solr.home', '/x/dev/workspace/test-solr/solr') + montysolr_java.System.setProperty('solr.data.dir', '/x/dev/workspace/test-solr/solr/data') + # montysolr_java.JettyRunner.main(()) + jetty = montysolr_java.JettyRunner() + jetty.start() + + + page = urllib2.urlopen('http://localhost:8983/test/select/?q=*%3A*&version=2.2&start=0&rows=10&indent=on&qt=recids').read() + print page + assert page.find('"numFound">6000') > -1 + start = page.index('name="docs">')+12 + docs = page[start:page.index("", start)].strip() + results = montysolr_java.ResultsCacheSingleton.getInstance().getResults(int(docs)) + + print 'this is printed by python but comes from java' + print results + + jetty.stop() + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/src/python/montysolr/tests/unittest_run_jetty.py b/src/python/montysolr/tests/unittest_run_jetty.py new file mode 100644 index 000000000..77613d639 --- /dev/null +++ b/src/python/montysolr/tests/unittest_run_jetty.py @@ -0,0 +1,28 @@ +''' +Created on Jan 10, 2011 + +@author: rca +''' +import unittest +import montysolr + + +class Test(unittest.TestCase): + + + def setUp(self): + sorlpie.initVM() + + + def tearDown(self): + pass + + + def test_jetty(self): + '''Tests if we are able to start jetty inside python and request results from the index''' + sorlpie.JettyRunner.main(('solr.home', '/x/dev/workspace/test-solr/solr')) + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/src/python/montysolr/utils.py b/src/python/montysolr/utils.py new file mode 100644 index 000000000..51f6cfc25 --- /dev/null +++ b/src/python/montysolr/utils.py @@ -0,0 +1,14 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +class MontySolrTarget(object): + def __init__(self, message_id, callable): + self._message_id = message_id + self._target = callable + def getTarget(self): + return self._target + def getMessageId(self): + return self._message_id \ No newline at end of file diff --git a/src/python/utils/attach_fulltexts.py b/src/python/utils/attach_fulltexts.py new file mode 100644 index 000000000..7b6ee7a97 --- /dev/null +++ b/src/python/utils/attach_fulltexts.py @@ -0,0 +1,186 @@ +''' +Created on Feb 3, 2011 + +Search the folders starting at point X, finds all the files of a certain +pattern and copy them to a different folders. + +@author: rca +''' +import sys +import os +import shutil +import logging as log +import subprocess + + +from invenio import search_engine, bibdocfile, bibdocfilecli +log.root.setLevel(5) + +MSG_AFTER = 100 +BATCH_SIZE = 1000 + + +def run(ids_file, src_dir, mode='append', suffix=".pdf"): + """Traverse the source folder, search for files, when found, + copies them to other place - into the properly ordered file-system + """ + + assert os.path.exists(src_dir) is True + + + if mode not in ('append', 'replace'): + raise Exception('Unknown mode ' + mode) + + ids_map, has_filepath = get_prescription(ids_file) + total_counter = [0, 0, 0] + + if has_filepath: + process_afs_folders(total_counter, ids_map, src_dir, append=(mode == 'append')) + else: + process_harvests(total_counter, ids_map, src_dir, append=(mode == 'append')) + + + print 'uploaded: %s, skipped: %s, not-found: %s' % (total_counter[0], total_counter[1], total_counter[2]) + + +def process_harvests(total_counter, ids_map, src_dir, suffix='.pdf', append=True): + files = filter(lambda x: x not in ['.', '..'], os.listdir(src_dir)) + i = 0 + ffts = {} + for name, (recid, arxiv_id) in ids_map.items(): + fullname = name + suffix + if fullname in files: + fullpath = os.path.join(src_dir, fullname) + res = prepare_ffts(ffts, recid, arxiv_id, fullpath, append=append) + if res == False: + total_counter[1] += 1 + else: + total_counter[0] += 1 + else: + total_counter[2] += 1 + + i += 1 + if i % MSG_AFTER == 0: + print 'processed %s out of %s' % (i, len(ids_map)) + if len(ffts) % BATCH_SIZE == 0: + upload_file(ffts) + + if len(ffts): + upload_file(ffts) + + + +def process_afs_folders(total_counter, ids_map, src_dir, suffix='.pdf', append=True): + i = 0 + ffts = [] + for name, (recid, topdir, arxiv_id) in ids_map.items(): + fullpath = os.path.join(src_dir, topdir, name + suffix) + if not os.path.exists(fullpath): + log.error("The file %s not exists" % fullpath) + total_counter[2] += 1 + continue + + res = prepare_ffts(ffts, recid, arxiv_id, fullpath, append=append) + if res == False: + total_counter[1] += 1 + else: + total_counter[0] += 1 + + i += 1 + if i % MSG_AFTER == 0: + print 'processed %s out of %s' % (i, len(ids_map)) + + if len(ffts) % BATCH_SIZE == 0: + upload_file(ffts) + + if len(ffts): + upload_file(ffts) + + + +def get_prescription(ids_file): + ids_map = {} + fi = open(ids_file, 'r') + # read the first line (find it contains filepath) + elems = fi.readline().strip().split('\t') + has_filepath = False + if len(elems) > 2: + has_filepth = True + fi.seek(0) + + if has_filepath: + for line in fi: + line = line.strip() + if not line: + continue + recid, arxiv_id, path = line.split('\t') + name , topdir = split_arxivid(arxiv_id) + if name and topdir: + ids_map[name] = (recid, topdir, arxiv_id) + else: + for line in fi: + line = line.strip() + if not line: + continue + recid, arxiv_id = line.split() + ids_map[arxiv_id.replace('/', '_')] = (recid, arxiv_id) + return (ids_map, has_filepath) + + + +def prepare_ffts(ffts, recid, docname, fullpath, doctype='arXiv', append=False, format='.pdf', options=['HIDDEN']): + recid = int(recid) + docname = 'arXiv:%s' % docname.replace('/', '_') + bibdoc = bibdocfile.BibRecDocs(recid) + + res = subprocess.Popen(['file', fullpath], stdout=subprocess.PIPE).communicate()[0] + if not ('PDF' in res or 'pdf' in res.lower()): + return False + + # check it is an existing recod + if len(bibdoc.display()) and (bibdoc.has_docname_p(docname) and append is not False): + return False + + ffts[recid] = [{ + 'docname' : docname, + 'format' : format, + 'url' : fullpath, + 'doctype': doctype, + 'options': options, + }] + + +def upload_file(ffts, append=True): + try: + sys.argv.append('--yes-i-know') + out = bibdocfilecli.bibupload_ffts(ffts, append=append, debug=False) + finally: + sys.argv.pop(-1) + ffts.clear() + + +def split_arxivid(arxiv_id, err=True): + name = topdir = None + if arxiv_id.find('/') > -1: + arx_parts = arxiv_id.split('/') #math-gt/060889 + name = ''.join(arx_parts) + topdir = arx_parts[1][:4] + elif arxiv_id.find('.') > -1: + arx_parts = arxiv_id.split('.', 1) #0712.0712 + topdir = arx_parts[0] + name = ''.join(arx_parts) + else: + if err: + print 'error parsing:', arxiv_id + return name, topdir + + + +if __name__ == '__main__': + if len(sys.argv) == 1 or not os.path.exists(sys.argv[1]): + try: + sys.argv[1] = int(sys.argv[1]) + except: + exit('Usage: find_fulltexts.py ') + print sys.argv[1:] + run(*sys.argv[1:]) diff --git a/src/python/utils/compress_top_folders.py b/src/python/utils/compress_top_folders.py new file mode 100644 index 000000000..7b5fc8feb --- /dev/null +++ b/src/python/utils/compress_top_folders.py @@ -0,0 +1,28 @@ +import sys +import os + +COMPRESS_CMD = 'tar -czf "%s.tgz" "%s"' +REMOVE_CMD = 'rm -fR "%s"' + +def run(src_dir, delete=False): + old_dir = os.getcwd() + os.chdir(src_dir) + files = os.listdir(src_dir) + for f in files: + if os.path.isdir(f): + #fullname = os.path.abspath(os.path.join(src_dir, f)) + fullname = f + cmd = COMPRESS_CMD % (fullname, fullname) + os.system(cmd) + if delete: + cmd = REMOVE_CMD % fullname + os.system(cmd) + print f + os.chdir(old_dir) + +if __name__ == '__main__': + if len(sys.argv) < 1: + exit('usage: program ') + if len(sys.argv) == 2: + sys.argv.append(False) + run(*sys.argv[1:]) \ No newline at end of file diff --git a/src/python/utils/copy_top_folders.py b/src/python/utils/copy_top_folders.py new file mode 100644 index 000000000..28ee2ab4f --- /dev/null +++ b/src/python/utils/copy_top_folders.py @@ -0,0 +1,31 @@ +import sys +import os + +COPY_CMD = 'cp -fR %s %s' +REMOVE_CMD = 'rm -fR %s' + +def run(src_dir, tgt_dir, delete=False): + files = os.listdir(src_dir) + tgt = os.path.abspath(tgt_dir) + for f in files: + if f != '.' or f != '..': + fullname = os.path.abspath(os.path.join(src_dir, f)) + + if delete: + existing = os.path.abspath(os.path.join(src_dir, f)) + if os.path.exists(existing): + print 'remove ' + existing + cmd = REMOVE_CMD % existing + os.system(cmd) + + cmd = COPY_CMD % (fullname, tgt) + os.system(cmd) + + print f + +if __name__ == '__main__': + if len(sys.argv) < 1: + exit('usage: program ') + if len(sys.argv) == 3: + sys.argv.append(False) + run(*sys.argv[1:]) \ No newline at end of file diff --git a/src/python/utils/decompress_top_folders.py b/src/python/utils/decompress_top_folders.py new file mode 100644 index 000000000..a0a156e09 --- /dev/null +++ b/src/python/utils/decompress_top_folders.py @@ -0,0 +1,28 @@ +import sys +import os + +DECOMPRESS_CMD = 'tar -xf %s' +REMOVE_CMD = 'rm -fR %s' + +def run(src_dir, delete=False): + old_dir = os.getcwd() + os.chdir(src_dir) + files = os.listdir(src_dir) + for f in files: + if os.path.isfile(f): + #fullname = os.path.abspath(os.path.join(src_dir, f)) + fullname = f + cmd = DECOMPRESS_CMD % (fullname,) + status = os.system(cmd) + if status == 0 and delete: + cmd = REMOVE_CMD % fullname + os.system(cmd) + print f + os.chdir(old_dir) + +if __name__ == '__main__': + if len(sys.argv) < 2: + exit('usage: program ') + if len(sys.argv) == 2: + sys.argv.append(False) + run(*sys.argv[1:]) \ No newline at end of file diff --git a/src/python/utils/dump_dicts.py b/src/python/utils/dump_dicts.py new file mode 100644 index 000000000..ba71b43d4 --- /dev/null +++ b/src/python/utils/dump_dicts.py @@ -0,0 +1,27 @@ +import os +import cPickle +from invenio import bibrank_citation_searcher as bcs +from invenio import intbitset + +'''Utility to dump cached citation dictionary into a filesystem''' + +basedir = '/opt/rchyla/citdicts' + +cit_names = ['citationdict', + 'reversedict', 'selfcitdict', 'selfcitedbydict'] + +for dname in cit_names: + print 'loading: %s' % dname + cd = bcs.get_citation_dict(dname) # load the dictionary + f = os.path.join(basedir, dname) # dump it out + fo = open(f, 'wb') + print 'dumping of %s entries started' % len(cd) + if isinstance(cd, intbitset): + cPickle.dump(cd.fastdump(), fo) + else: + cPickle.dump(cd, fo) + fo.close() + print 'dumped %s into %s' % (dname, f) + + + diff --git a/src/python/utils/extract_queries.py b/src/python/utils/extract_queries.py new file mode 100644 index 000000000..e533b2d1f --- /dev/null +++ b/src/python/utils/extract_queries.py @@ -0,0 +1,194 @@ +import time +import os +import sys +import re + +_d = '/opt/invenio/lib/python' +if _d not in sys.path: + sys.path.insert(0, _d) + +from invenio import search_engine_query_parser + +invenio_qparser = search_engine_query_parser.SearchQueryParenthesisedParser() +invenio_qconverter = search_engine_query_parser.SpiresToInvenioSyntaxConverter() + +def run(searchlog_file): + """This will extract some known query patterns form the + the search logs + """ + + # find the last recid, that we indexed + out_filepath = os.path.join(os.path.dirname(searchlog_file), 'searchlog-%s' % os.path.split(str(searchlog_file))[1]) + + fi = open(searchlog_file, 'r') + fo = open(out_filepath, 'w') + + i = 0 + for line in fi: + # 20110101004545#ss#find a trnka, jaroslav##HEP#16 + line = line.strip() + parts = line.split('#') + if len(parts) != 6: + continue + fdate, fform, fvalue, ffield, fcollection, fresults = parts + q = convert_query(fvalue, ffield) + if q: + fo.write('%s\n' % q) + + i+= 1 + if 1 % 1000 == 0: + print i + continue + fo.close() + + if 0: + val = None + if ffield: #the field was specified + if ffield == 'author': + val = format_author(fvalue) + elif ffield == 'exactauthor': + val = format_exactauthor(fvalue) + elif ffield == 'fulltext': + val = format_fulltext(fvalue) + elif ffield == 'journal': + val = format_journal(fvalue) + elif ffield == 'title': + val = format_title(fvalue) + elif ffield == 'keyword': + val = format_keyword(fvalue) + elif ffield == 'year': + val = format_year(fvalue) + + if val: + fo.write('%s\n' % val) + +_regexes = [] +def get_query_regexes(): + global _regexes + if _regexes: + return _regexes + _regexes.extend([ + (re.compile(r'001\s*\:'), 'recid:'), + (re.compile(r'980\s*\:'), 'status:'), + + (re.compile(r'100__u\:'), 'affiliation:'), + (re.compile(r'700__u\:'), 'affiliation:'), + (re.compile(r'902__a\:'), 'affiliation:'), + + (re.compile(r'100\s*\:'), 'author:'), + (re.compile(r'700\s*\:'), 'author:'), + + (re.compile(r'710\s*\:'), 'corporation:'), + + (re.compile(r'773__a\:'), 'doi:'), + (re.compile(r'773\s*\:'), 'publication:'), + + (re.compile(r'037*\:'), 'reportnumber:'), + (re.compile(r'245_*\s*\:'), 'title:'), + + (re.compile(r'035__z\:'), 'other_id:'), + #(re.compile(r':\s*\*'), ':'), #we don't allow asterisk at the start + ] + + ) + inspire_fields = { + 'eprint':'reportnumber', + 'bb':'reportnumber', + 'bbn':'reportnumber', + 'bull':'reportnumber', + 'r':'reportnumber', + 'rn':'reportnumber', + 'cn':'collaboration', + 'a':'author', + 'au':'author', + 'name':'author', + 'ea':'exactauthor', + 'exp':'experiment', + 'expno':'experiment', + 'sd':'experiment', + 'se':'experiment', + 'j':'publication', #was journal + 'kw':'keyword', + 'keywords':'keyword', + 'k':'keyword', + 'au':'author', + 'ti':'title', + 't':'title', + 'irn':'970__a', + 'institution':'affiliation', + 'inst':'affiliation', + 'affil':'affiliation', + 'aff':'affiliation', + 'af':'affiliation', + '902_*.*': 'affiliation', + '695__a':'topic', + 'tp':'695__a', + 'dk':'695__a', #'topic':'695__a','tp':'695__a','dk':'695__a', + 'date':'year', + 'd':'year', + 'date-added':'datecreated', + 'da':'datecreated', + 'dadd':'datecreated', + 'date-updated':'datemodified', + 'dupd':'datemodified', + 'du':'datemodified' + } + for k, v in inspire_fields.items(): + _regexes.append( + (re.compile('\W%s:' % k), '%s:' % v) + ) + return _regexes + + + +def convert_query(p, field=None): + # if the pattern uses SPIRES search syntax, convert it to Invenio syntax + if invenio_qconverter.is_applicable(p): + p = invenio_qconverter.convert_query(p) + p = p.strip() + + # do some basic transformations + _transregex = get_query_regexes() + + field = field.strip() + if field and p[0:len(field)] != field: + p = '%s:%s' % (field, p) + + for regex, replacement in _transregex: + p = regex.sub(replacement, p) + + return p + + +def format_author(s): + s = s.replace('find author ', '').replace('find a ').replace('f k ') + return 'author:(%s)' % s.strip() + +def format_exactauthor(s): + return 'author:"%s"' % format_author(s) + +def format_fulltext(s): + return 'text:%s' % s.strip() + +def format_journal(s): + s = s.replace('find journal ').replace('find f ').replace('f j ') + return 'publication:%s' % s + +def format_title(s): + s = s.replace('find title ').replace('find t ').replace('f t ') + return 'title:%s' % s + +def format_keyword(s): + s = s.replace('find keyword ').replace('find k ').replace('f k ') + return 'title:%s' % s + +def format_year(s): + if s.isalnum(): + return '' + return 'date:(%s)' % s.strip() + + +if __name__ == '__main__': + if len(sys.argv) == 1 or not os.path.exists(sys.argv[1]): + exit('Usage: extract_queries.py ') + run(sys.argv[1]) diff --git a/src/python/utils/find_fulltexts.py b/src/python/utils/find_fulltexts.py new file mode 100644 index 000000000..74c371d69 --- /dev/null +++ b/src/python/utils/find_fulltexts.py @@ -0,0 +1,159 @@ +''' +Created on Feb 3, 2011 + +Search the folders starting at point X, finds all the files of a certain +pattern and copy them to a different folders. + +@author: rca +''' +import sys +import os +import shutil + +import logging as log + +log.root.setLevel(5) + + +def run(idsfile, src_dir, tgt_dir, mode, extensions): + """Traverse the source folder, search for files, when found, + copies them to other place - into the properly ordered file-system + """ + + assert os.path.exists(src_dir) is True + assert os.path.exists(tgt_dir) is True + + extensions = extensions.split(',') + assert len(extensions) > 0 + + print 'we will search for these extensions:' + '|'.join(extensions) + + stack = {} + stack['found'] = open(os.path.join('/tmp', 'found.txt'), 'w') + stack['not-found'] = open(os.path.join('/tmp', 'not-found.txt'), 'w') + + if mode not in ('copy', '#copy', 'count'): + raise Exception('Unknown mode ' + mode) + + ids_map = {} + fi = open(idsfile, 'r') + for line in fi: + line = line.strip() + if not line: + continue + recid, arxiv_id = line.split('\t') + name , topdir = split_arxivid(arxiv_id) + if name and topdir: + ids_map[name] = (recid, topdir, line) + + total_counter = [0] + created_target_dirs = [] + + def copy_func(arg, dirname, fnames): + + log.info('inside: %s' % dirname) + to_copy = {} + for f in fnames: + fullpath = os.path.join(dirname, f) + basename, ext = os.path.splitext(f) + if basename and ext: + _e = ext[1:].lower() #remove the leading dot + if extensions and _e in extensions: + name, topdir = split_arxivid(basename, err=False) + found = False + if basename in ids_map: + name = basename + topdir = ids_map[basename][1] + found = True + elif name in ids_map: + name = name + topdir = ids_map[name][1] + found = True + + if found: + if mode[0] == '#': + continue + else: + if mode[0] == '#': + # we are looking for files not in the list + topdir = os.path.split(dirname)[1] + else: + continue + + target = os.path.join(tgt_dir, _e, topdir) + + if _e == 'txt' or _e == 'utf8': + to_copy[name] = (os.path.join(dirname, f), target, f) + elif _e == 'pdf': + if name not in to_copy: # txt files have preference + to_copy[name] = (os.path.join(dirname, f), target, f) + log.info('identified: %s candidates' % len(to_copy)) + + if mode == 'copy' or mode == '#copy': + for name, (source, target, filename) in to_copy.items(): + if target not in created_target_dirs and not os.path.isdir(target): + os.makedirs(target) + created_target_dirs.append(target) + try: + target_file = os.path.join(target, filename) + if not os.path.exists(target_file): + shutil.copy(source, target) + del ids_map[name] + total_counter[0] += 1 + except Exception, msg: + del to_copy[name] + if os.path.isdir(source): + pass + else: + print msg + if len(to_copy): + print 'copied: %s (in total so far: %s)' % (len(to_copy), total_counter[0]) + elif mode == 'count': + for name, (source, target, filename) in to_copy.items(): + record = ids_map.pop(name) + stack['found'].write('%s\t%s\n' % (record[2], source)) + + print '%s: %s' % (dirname, len(to_copy)) + + try: + os.path.walk(src_dir, copy_func, None) + except KeyboardInterrupt: + pass + + print 'found: %s, not-found: %s' % (total_counter[0], len(ids_map)) + fo = stack['not-found'] + for k, (recid, topdir, line) in ids_map.items(): + fo.write('%s\n' % line) + fo.close() + stack['found'].close() + + target_f = os.path.join(tgt_dir, 'found.txt') + target_nf = os.path.join(tgt_dir, 'not-found.txt') + + shutil.copyfile(stack['found'].name, target_f) + shutil.copyfile(stack['found'].name, target_nf) + + +def split_arxivid(arxiv_id, err=True): + name = topdir = None + if arxiv_id.find('/') > -1: + arx_parts = arxiv_id.split('/') #math-gt/060889 + name = ''.join(arx_parts) + topdir = arx_parts[1][:4] + elif arxiv_id.find('.') > -1: + arx_parts = arxiv_id.split('.', 1) #0712.0712 + topdir = arx_parts[0] + name = ''.join(arx_parts) + else: + if err: + print 'error parsing:', arxiv_id + return name, topdir + +if __name__ == '__main__': + if len(sys.argv) == 1 or not os.path.exists(sys.argv[1]): + try: + sys.argv[1] = int(sys.argv[1]) + except: + exit('Usage: find_fulltexts.py %s<' % recid) > -1: + + if newdir not in existing_dirs: + if not os.path.exists(newdir): + os.makedirs(newdir) + existing_dirs[newdir] = True + + fo = open(newfile, 'w') + fo.write(text) + fo.close() + recid_file.seek(0) + recid_file.write(recid) + recid_file.flush() + except: + print 'error getting: %s' % u + continue + i += 1 + + + + +if __name__ == '__main__': + if len(sys.argv) == 1 or not os.path.exists(sys.argv[1]): + try: + sys.argv[1] = int(sys.argv[1]) + except: + exit('Usage: run_index.py ') + + if not os.path.exists(str(sys.argv[1])): + try: + x = int(sys.argv[1]) + print 'Harvesting a range: 0-%s' % x + run(x) + except: + run(sys.argv[1]) + else: + run(sys.argv[1]) + + + diff --git a/src/python/utils/import_dicts.py b/src/python/utils/import_dicts.py new file mode 100644 index 000000000..aef0a7537 --- /dev/null +++ b/src/python/utils/import_dicts.py @@ -0,0 +1,54 @@ + +import sys, os +import cPickle +from invenio import bibrank_citation_indexer as bci +from invenio import intbitset +import time + +'''Utility to import pickled cached citation dictionary into a database. +WARNING! This will replace your database entries!!! ''' + +if len(sys.argv) > 1: + basedir = sys.argv[1] +else: + basedir = '/opt/rchyla/citdicts' + +if not os.path.exists(basedir) and not os.path.isdir(basedir): + raise Exception('%s is not a folder' % basedir) + +cit_names = ['citationdict', + 'reversedict', 'selfcitdict', 'selfcitedbydict'] + +def insert_into_cit_db(dic, name): + """an aux thing to avoid repeating code""" + ndate = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + s = bci.serialize_via_marshal(dic) + #check that this column really exists + testres = bci.run_sql("select object_name from rnkCITATIONDATA where object_name = %s", + (name,)) + if testres: + bci.run_sql("UPDATE rnkCITATIONDATA SET object_value = %s where object_name = %s", + (s, name)) + else: + #there was no entry for name, let's force.. + bci.run_sql("INSERT INTO rnkCITATIONDATA(object_name,object_value) values (%s,%s)", + (name,s)) + bci.run_sql("UPDATE rnkCITATIONDATA SET last_updated = %s where object_name = %s", + (ndate,name)) + + +for dname in cit_names: + # load the dictionary + f = os.path.join(basedir, dname) + if os.path.exists(f): + print 'loading %s...' % dname + fi = open(f, 'rb') + cd = cPickle.load(fi) + if isinstance(cd, basestring): + cd = intbitset.intbitset().fastload(cd) + fi.close() + print 'loaded %s made of %s entries' % (dname, len(cd)) + print 'saving into db...' + insert_into_cit_db(cd, dname) + print 'saved!' + diff --git a/src/python/utils/run_index.py b/src/python/utils/run_index.py new file mode 100644 index 000000000..a87487431 --- /dev/null +++ b/src/python/utils/run_index.py @@ -0,0 +1,135 @@ +import urllib2 +import time +import os +import sys +import subprocess + +SOLRURL = 'http://localhost:8984/solr/waiting-dataimport?command=full-import&url=%(docaddr)s&%(extraparam)s' +DOCADDR = 'file://%(datadir)s/metadata/%(topdir)s/%(recid)s' +EXTRAPARAM = 'dirs=%(datadir)s/fulltexts/arXiv' +COMMIT = 1000000 + +BASKET_SIZE = 1000 +CHECK_IN_ADVANCE = 1000 +EXTRACT_CMD = 'tar -C %(datadir)s/metadata -xf %(datadir)s/metadata/%(topdir)s.tgz' +REMOVE_CMD = 'rm -fR %(datadir)s/metadata/%(topdir)s' + +def run(recids_file, datadir): + """This will call the indexer - passing recid on every call, + if the passed in argument is an integer, then we can work + without recids, just using the range (0, recids) + """ + commit_after = COMMIT + + # find the last recid, that we indexed + recid_filepath = os.path.join(datadir, 'last-recid-%s' % os.path.split(str(recids_file))[1]) + #os.remove(recid_filepath) + last_recid = 1 + if os.path.exists(recid_filepath): + recid_file = open(recid_filepath, 'r') + x = recid_file.read().strip() + recid_file.close() + if x: + last_recid = x + + + if isinstance(recids_file, int): + try: + recids = range(int(last_recid), recids_file) + except: + raise Exception('If you want to index a range, last-recid must be a number, not: "%s"' % last_recid) + else: + recids = [] + for r in open(recids_file, 'r'): + recids.append(r.strip()) + if str(last_recid) in recids: + _i = recids.index(str(last_recid)) + recids = recids[_i+1:] + + # we will write the last-id into this file + recid_file = open(recid_filepath, 'w') + + + start_time = time.time() + last_extracted_topdir = None + last_id = recids[-1] + + _for_removal = [] + i = 0 + _success = _failure = 0 + params = {'datadir': datadir} + params['extraparam'] = EXTRAPARAM % params + for recid in recids: + params['topdir'] = int(int(recid) / BASKET_SIZE) + params['recid'] = recid + params['docaddr'] = DOCADDR % params + + u = SOLRURL % params + if i % commit_after == 0 or recid == last_id: + u += '&commit=true' + print u + + # look at the files ahead and if necessary + # extract the archive (without waiting) + if EXTRACT_CMD: + if datadir and last_extracted_topdir is None: + args = EXTRACT_CMD % params + pid = subprocess.Popen(args.split()).pid + if REMOVE_CMD: + _for_removal.insert(0, REMOVE_CMD % params) + last_extracted_topdir = params['topdir'] + _n = i+CHECK_IN_ADVANCE + if _n < len(recids): + next_topdir = int(int(recids[_n]) / BASKET_SIZE) + if next_topdir != last_extracted_topdir: + old_topdir = params['topdir'] + params['topdir'] = next_topdir + args = EXTRACT_CMD % params + # run extraction + pid = subprocess.Popen(args.split()).pid + last_extracted_topdir = next_topdir + if REMOVE_CMD: + _for_removal.insert(0, REMOVE_CMD % params) + if len(_for_removal) > 2: + #remove the folder again + subprocess.Popen(_for_removal.pop().split()).pid + params['topdir'] = old_topdir + + while True: + text = urllib2.urlopen(u).read() + #print text + if text.find('>idle -1: + if text.find('Rolledback') > -1: + print 'not indexed: %s/%s' % (params['topdir'], params['recid']) + _failure += 1 + else: + _success += 1 + break + else: + print 'sleeping' + time.sleep(.1) + i += 1 + total_time = time.time() - start_time + avg_time = total_time / i + if i % 100 == 0: + print '%s.\t%s\t%s/%s\t%s\t%s' % (i, recid, _success, _failure, '%5.3f h.' % (total_time / 3600), avg_time) + + recid_file.seek(0) + recid_file.write(str(recid)) + recid_file.flush() + + print '%s.\t%s\t%s/%s\t%s\t%s' % (i, recid, _success, _failure, '%5.3f h.' % (total_time / 3600), avg_time) + recid_file.close() + for r in _for_removal: + subprocess.Popen(r.split()).pid + + + +if __name__ == '__main__': + if len(sys.argv) < 2 or not (os.path.exists(sys.argv[1]) and os.path.exists(sys.argv[2])): + try: + sys.argv[1] = int(sys.argv[1]) + except: + exit('Usage: run_index.py ') + + run(*sys.argv[1:]) diff --git a/test/java/invenio/montysolr/MontySolrTestCase.java b/test/java/invenio/montysolr/MontySolrTestCase.java new file mode 100644 index 000000000..f04727058 --- /dev/null +++ b/test/java/invenio/montysolr/MontySolrTestCase.java @@ -0,0 +1,96 @@ +package invenio.montysolr; + +import invenio.montysolr.jni.PythonBridge; +import invenio.montysolr.jni.MontySolrVM; + +import java.io.PrintStream; +import java.io.File; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; + +import junit.framework.TestCase; + + + +/** + * Base class for all Lucene unit tests. + *

+ * Currently the + * only added functionality over JUnit's TestCase is + * asserting that no unhandled exceptions occurred in + * threads launched by ConcurrentMergeScheduler and asserting sane + * FieldCache usage athe moment of tearDown. + *

+ *

+ * If you + * override either setUp() or + * tearDown() in your unit test, make sure you + * call super.setUp() and + * super.tearDown() + *

+ * @see #assertSaneFieldCaches + */ +public abstract class MontySolrTestCase extends TestCase { + + protected MontySolrVM VM; + + public MontySolrTestCase() { + super(); + } + + public MontySolrTestCase(String name) { + super(name); + } + + protected void setUp() throws Exception { + MontySolrVM.INSTANCE.start("montysolr_java"); + this.VM = MontySolrVM.INSTANCE; + super.setUp(); + + } + + protected PythonBridge getBridge() { + return MontySolrVM.INSTANCE.getBridge(); + } + + protected String getTestLabel() { + return getClass().getName() + "." + getName(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + + /** + * Convinience method for logging an iterator. + * @param label String logged before/after the items in the iterator + * @param iter Each next() is toString()ed and logged on it's own line. If iter is null this is logged differnetly then an empty iterator. + * @param stream Stream to log messages to. + */ + public static void dumpIterator(String label, Iterator iter, + PrintStream stream) { + stream.println("*** BEGIN "+label+" ***"); + if (null == iter) { + stream.println(" ... NULL ..."); + } else { + while (iter.hasNext()) { + stream.println(iter.next().toString()); + } + } + stream.println("*** END "+label+" ***"); + } + + /** + * Convinience method for logging an array. Wraps the array in an iterator and delegates + * @see dumpIterator(String,Iterator,PrintStream) + */ + public static void dumpArray(String label, Object[] objs, + PrintStream stream) { + Iterator iter = (null == objs) ? null : Arrays.asList(objs).iterator(); + dumpIterator(label, iter, stream); + } + +} + diff --git a/test/java/org/apache/solr/search/TestInvenioQueryParser.java b/test/java/org/apache/solr/search/TestInvenioQueryParser.java new file mode 100644 index 000000000..e843fb366 --- /dev/null +++ b/test/java/org/apache/solr/search/TestInvenioQueryParser.java @@ -0,0 +1,312 @@ +package org.apache.solr.search; + +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.search.Query; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.core.SolrCore; +import org.apache.solr.util.AbstractSolrTestCase; + +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.schema.IndexSchema; +import org.apache.solr.search.InvenioQParserPlugin; + +public class TestInvenioQueryParser extends AbstractSolrTestCase { + + public String getSchemaFile() { return "schema.xml"; } + public String getSolrConfigFile() { return "solrconfig.xml"; } + public String getCoreName() { return "basic"; } + + + public void setUp() throws Exception { + // if you override setUp or tearDown, you better call + // the super classes version + super.setUp(); + } + public void tearDown() throws Exception { + // if you override setUp or tearDown, you better call + // the super classes version + super.tearDown(); + } + + + public void testQueryTypes() { + assertU(adoc("id","1", "v_t","Hello Dude")); + assertU(adoc("id","2", "v_t","Hello Yonik")); + assertU(adoc("id","3", "v_s","{!literal}")); + assertU(adoc("id","4", "v_s","other stuff")); + assertU(adoc("id","5", "v_f","3.14159")); + assertU(adoc("id","6", "v_f","8983")); + assertU(adoc("id","7", "v_f","1.5")); + assertU(adoc("id","8", "v_ti","5")); + assertU(commit()); + + Object[] arr = new Object[] { + "id",999.0 + ,"v_s","wow dude" + ,"v_t","wow" + ,"v_ti",-1 + ,"v_tis",-1 + ,"v_tl",-1234567891234567890L + ,"v_tls",-1234567891234567890L + ,"v_tf",-2.0f + ,"v_tfs",-2.0f + ,"v_td",-2.0 + ,"v_tds",-2.0 + ,"v_tdt","2000-05-10T01:01:01Z" + ,"v_tdts","2002-08-26T01:01:01Z" + }; + + + + SolrCore core = h.getCore(); + SolrQueryRequest req = lrf.makeRequest("q", "*:*"); + SolrParams params = req.getParams(); + ModifiableSolrParams localParams = new ModifiableSolrParams(); + localParams.add("iq.mode", "maxinv"); + localParams.add("iq.xfields", "fulltext,abstract"); + localParams.add("iq.syntax", "lucene"); + + ModifiableSolrParams localParams_2 = new ModifiableSolrParams(); + localParams_2.add("iq.mode", "maxinv"); + localParams_2.add("iq.xfields", "fulltext,abstract"); + localParams_2.add("iq.syntax", "invenio"); + + assertTrue("core is null and it shouldn't be", core != null); + QParserPlugin parserPlugin = core.getQueryPlugin(InvenioQParserPlugin.NAME); + + + String[] queries = { + + "author:hawking and affiliation:\"cambridge u., damtp\" and year:2004->9999", + " + +9999>", + + // test cases + "hey |muon", + " ", + "hey |\"muon muon\"", + " <\"muon muon\">", + "\"and or not AND OR NOT\" and phrase", + "<\"and or not and or not\"> +", + + + // http://inspirebeta.net/help/search-tips + + // find a hawking and aff "cambridge u., damtp" and date > 2004 + "author:hawking and affiliation:\"cambridge u., damtp\" and year:2004->9999", + " + +9999>", + "thomas crewther quark 2002", + " <2002>", + //find j phys.rev.lett.,62,1825 + "journal:phys.rev.lett.,62,1825", + "", + //find j "Phys.Rev.Lett.,105*" or j Phys.Lett. and a thomas + "journal:\"Phys.Rev.Lett.,105*\" or journal:Phys.Lett. and author:thomas", + " +", + // find d 1997-11-18 + "year:1997-11-18", + "", + // find da 2011-01-26 and title neutrino* + "datecreated:2011-01-26 and title:neutrino*", + " +", + //find eprint arxiv:0711.2908 or arxiv:0705.4298 or eprint hep-ph/0504227 + "reportnumber:arxiv:0711.2908 or arxiv:0705.4298 or reportnumber:hep-ph/0504227", + "", + //find a unruh or t cauchy not t problem and primarch gr-qc + "author:unruh or title:cauchy not title:problem and 037__c:gr-qc", + " - +<037__c|gr-qc>", + //find a m albrow and j phys.rev.lett. and t quark* cited:200->99999 + "(author:\"albrow, m*\") and journal:phys.rev.lett. and (title:quark* and title:cited:200->99999)", + "", + //find c Phys.Rev.Lett.,28,1421 or c arXiv:0711.4556 + "reference:Phys.Rev.Lett.,28,1421 or reference:arXiv:0711.4556", + "", + //find c "Phys.Rev.Lett.,*" + "reference:\"Phys.Rev.Lett.,*\"", + "", + //citedby:hep-th/9711200 author:cvetic + "citedby:hep-th/9711200 author:cvetic", + " ", + "author:parke citedby:author:witten", + "", + "refersto:hep-th/9711200 title:nucl*", + " ", + "author:witten refersto:author:\"parke, s j\"", + "", + "refersto:author:parke or refersto:author:lykken author:witten", + "", + "affiliation:\"oxford u.\" refersto:title:muon*", + "", + // find af "harvard u." + "affiliation:\"harvard u.\"", + "", + + // http://inspirebeta.net/help/search-guide + + "\"Ellis, J\"", + "<\"ellis, j\">", + "'muon decay'", + "<'muon decay'>", + "'Ellis, J'", + "<'ellis, j'>", + "ellis +muon", + " +", + "ellis muon", + " ", + "ellis and muon", + " +", + "ellis -muon", + " -", + "ellis not muon", + " -", + "ellis |muon", + " ", + "ellis or muon", + " ", + "muon or kaon and ellis", + " +", + "ellis and muon or kaon", + " + ", + "muon or kaon and ellis -decay", + " + -", + "(gravity OR supergravity) AND (ellis OR perelstein)", + "( ) +( )", + "C++", + "", + "O'Shea", + "", + "$e^{+}e^{-}$", + "<$e^{+}e^{-}$>", + "hep-ph/0204133", + "", + "BlaCK hOlEs", + " ", + "пушкин", + "<пушкин>", + "muon*", + "", + "CERN-TH*31", + "", + "a*", + "", + "\"Neutrino mass*\"", + "<\"neutrino mass*\">", + "author:ellis", + "", + "author:ellis title:muon*", + " ", + "experiment:NA60 year:2001", + " ", + "title:/^E.*s$/", + "", + "author:/^Ellis, (J|John)$/", + "", + "title:/dense ([^ l]* )?matter/", + "", //TODO: remove the quotation marks + "collection:PREPRINT -year:/^[0-9]{4}([\\?\\-]|\\-[0-9]{4})?$/", + " -", + "collection:PREPRINT -year:/^[[:digit:]]{4}([\\?\\-]|\\-[[:digit:]]{4})?$/", + " -", + "muon decay year:1983->1992", + " 1992>", + "author:\"Ellis, J\"->\"Ellis, Qqq\"", + "\"ellis, qqq\">", + "refersto:reportnumber:hep-th/0201100", + "", + "citedby:author:klebanov", + "", + "refersto:author:\"Klebanov, I\"", + "", + "refersto:keyword:gravitino", + "", + "author:klebanov AND citedby:author:papadimitriou NOT refersto:author:papadimitriou", + "", + "refersto:/author:\"Klebanov, I\" title:O(N)/", + "", + "author:ellis -muon* +abstract:'dense quark matter' year:200*", + " - +abstract:\"dense quark matter\"~2 ", + "author:ellis -muon* +title:'dense quark matter' year:200*", + " - + ", + "higgs or reference:higgs or fulltext:higgs", + " fulltext:higgs", + "author:lin fulltext:Schwarzschild fulltext:AdS reference:\"Adv. Theor. Math. Phys.\"", + " fulltext:schwarzschild fulltext:ads ", + "author:/^Ellis, (J|John)$/", + "", + "fulltext:e-", + "fulltext:e-", + "muon or fulltext:muon and author:ellis", + " fulltext:muon +", + "reference:hep-ph/0103062", + "", + "reference:giddings reference:ross reference:\"Phys. Rev., D\" reference:61 reference:2000", + " ", + "standard model -author:ellis reference:ellis", + " - ", + + }; + + Query q; + Query q2; + QParser qp; + QParser qp2; + int success = 0; + for (int i=0; i" % (sys.argv[0],)) + run(sys.argv[1]) \ No newline at end of file diff --git a/test/python/test_examples_twitter.py b/test/python/test_examples_twitter.py new file mode 100644 index 000000000..993621e7a --- /dev/null +++ b/test/python/test_examples_twitter.py @@ -0,0 +1,47 @@ + +import unittest +from montysolr_testcase import MontySolrTestCase, sj + +import os +import time +import sys + + +class Test(MontySolrTestCase): + + def setUp(self): + self.setSolrHome(os.path.join(self.getBaseDir(), 'examples/twitter/solr')) + self.setDataDir(os.path.join(self.getBaseDir(), 'examples/twitter/solr/data')) + self.setHandler(self.loadHandler('montysolr.examples.twitter_test')) + MontySolrTestCase.setUp(self) + + + def test_twitter(self): + '''Index docs fetched by twitter api''' + + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'search') + hm.put('term', 'Feb17') + params = sj.MapSolrParams(hm) + + req = sj.LocalSolrQueryRequest(self.core, params) + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('twitter_api') \ + .setSender('TwitterAPIHandler') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + + res = sj.JArray_int.cast_(message.getResults()) + res = list(res) + assert len(res) == size + assert res[0] == 0 + assert res[5] == 5 + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.test_get_recids_changes4'] + unittest.main() diff --git a/test/python/test_invenio_queries.py b/test/python/test_invenio_queries.py new file mode 100644 index 000000000..f9f5cba9e --- /dev/null +++ b/test/python/test_invenio_queries.py @@ -0,0 +1,55 @@ + +import sys +import os +import solr + +from invenio import search_engine + +s = solr.SolrConnection('http://localhost:8983/solr') +s.select = solr.SearchHandler(s, '/invenio') + + +def run(query_file): + + fi = open(query_file, 'r') + queries = filter(len, map(lambda x: x.strip(), fi.readlines())) + fi.close() + + success = failure = error = 0 + for q in queries: + print '---' + print q + inv_res = len(search_engine.perform_request_search(None, p=q)) + msg = 'NO' + inv_query = '\t\t' + try: + (solr_res, inv_query) = ask_solr(q) + except Exception, e: + solr_res = None + #print e + msg = 'ER' + error += 1 + failure -= 1 + + print inv_query + if inv_res == solr_res: + success += 1 + msg = 'OK' + else: + failure += 1 + + + print "%s invenio=%s montysolr=%s" % (msg, inv_res, solr_res) + + print 'total=%s, success/mismatch/error=%s/%s/%s' % (len(queries), success, failure, error) + +def ask_solr(q): + response = s.query(q, fields=['id']) + num_found = response.numFound + inv_query = response.inv_query + return (num_found, inv_query) + +if __name__ == '__main__': + if len(sys.argv) < 2: + exit('Usage: ') + run(*sys.argv[1:]) diff --git a/test/python/testing_targets.py b/test/python/testing_targets.py new file mode 100644 index 000000000..34f045954 --- /dev/null +++ b/test/python/testing_targets.py @@ -0,0 +1,51 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +from montysolr.utils import MontySolrTarget +import os +from montysolr import initvm + +sj = initvm.montysolr_java + + +from invenio import bibrank_citation_searcher as bcs + + +def handle_request_body(message): + rsp = message.getSolrQueryResponse() + rsp.add("python", 'says hello!') + +def receive_field_value(message): + val = message.getParam('value') + val = sj.JArray_string.cast_(val) + val.append('z') + +def get_citation_dict(message): + dictname = sj.String.cast_(message.getParam('dictname')) + cd = bcs.get_citation_dict(dictname) + if cd: + hm = sj.HashMap().of_(sj.String, sj.JArray_int) + + for k,v in cd.items(): + j_array = sj.JArray_int(v) + hm.put(k, j_array) + + message.put('result', hm) + + + + + + +def montysolr_targets(): + targets = [ + MontySolrTarget('receive_field_value', receive_field_value), + MontySolrTarget('handleRequestBody', handle_request_body), + MontySolrTarget('CitationQuery:get_citation_dict', get_citation_dict), + ] + + return targets + \ No newline at end of file diff --git a/test/python/tmp_run_solr.py b/test/python/tmp_run_solr.py new file mode 100644 index 000000000..bba2c4ec9 --- /dev/null +++ b/test/python/tmp_run_solr.py @@ -0,0 +1,40 @@ + +def run(): + import unittest + import unittest_solr + #fo = open('/tmp/solr-test', 'w') + #suite = unittest.TestLoader().loadTestsFromTestCase(unittest_solr.Test) + #unittest.TextTestRunner(verbosity=2).run(suite) + #fo.write('OK!') + #fo.close() + + sj = unittest_solr.sj + + initializer = sj.CoreContainer.Initializer() + conf = {'solr_home': '/x/dev/workspace/sandbox/montysolr/example/solr', + 'data_dir': '/x/dev/workspace/sandbox/montysolr/example/solr/data-test'} + + sj.System.setProperty('solr.solr.home', conf['solr_home']) + sj.System.setProperty('solr.data.dir', conf['data_dir']) + core_container = initializer.initialize() + server = sj.EmbeddedSolrServer(core_container, "") + + solr_config = sj.SolrConfig() + index_schema = sj.IndexSchema(solr_config, None, None) + q = sj.QueryParsing.parseQuery('*:*', index_schema) + + # create a query + query = sj.SolrQuery() + query.setQuery('*:*') + + query_response = server.query(query) + + head_part = query_response.getResponseHeader() + res_part = query_response.getResults() + qtime = query_response.getQTime() + etime = query_response.getElapsedTime() + + print qtime, etime, head_part, res_part + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/test/python/unittest_bridge.py b/test/python/unittest_bridge.py new file mode 100644 index 000000000..4707ab5bd --- /dev/null +++ b/test/python/unittest_bridge.py @@ -0,0 +1,44 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' +import unittest +from montysolr import initvm, java_bridge +from montysolr import handler +import sys +import os + +sj = java_bridge.sj + +class TestHandler(handler.Handler): + def init(self): + self.discover_targets([os.path.join(os.path.basedir(__file__), 'testing_targets.py')]) + +class Test(unittest.TestCase): + + + def setUp(self): + self.bridge = java_bridge.SimpleBridge() + + def tearDown(self): + pass + + def test_basic(self): + b = self.bridge + + + assert b.testReturnString().find('java is printing') > -1 + assert b.getName() is None # the bridge has name only when started from java + + message = sj.PythonMessage('receive_field_value').setParam('value', sj.JArray_string(['x','z'])) + b.receive_message(message) + ret = message.getParam('result') + if ret: + r = list(ret) + assert r == ['x', 'z'] + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/python/unittest_examples_bigtest.py b/test/python/unittest_examples_bigtest.py new file mode 100644 index 000000000..e832f6029 --- /dev/null +++ b/test/python/unittest_examples_bigtest.py @@ -0,0 +1,214 @@ +''' +Created on May 11, 2011 + +@author: rca + +To run this unittest, you will need a lot of memory (if size is big) +You can do this: + +export MONTYSOLR_JVMARGS_PYTHON='-Xmx800m -d32' +python unittest_example_bigtest.py Test.test_bigtest01 +''' +#@PydevCodeAnalysisIgnore + +import unittest +from montysolr import initvm, java_bridge, handler +import os +import time +import sys + +sj = initvm.montysolr_java + +class TestHandler(handler.Handler): + def init(self): + #_b = os.path.join(os.path.dirname(__file__), 'testing_targets.py') + #self.discover_targets([_b]) + self.discover_targets(['montysolr.examples.bigtest']) +test_handler = TestHandler() + +class Test(unittest.TestCase): + + + def setUp(self): + self.size = 5000000 + sj.System.setProperty('solr.solr.home', os.path.join(os.path.abspath(os.path.dirname(initvm.__file__) + '../../..'), 'examples/twitter/solr')) + self.bridge = java_bridge.SimpleBridge(test_handler) + self.core = sj.SolrCore.getSolrCore() + + def tearDown(self): + #self.core.close() + pass + + + + def test_bigtest01(self): + '''Get int[]''' + + #req = sj.QueryRequest() + size = self.size + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'recids_int') + hm.put('size', str(size)) + params = sj.MapSolrParams(hm) + req = sj.LocalSolrQueryRequest(self.core, params) + + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('bigtest') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + + res = sj.JArray_int.cast_(message.getResults()) + res = list(res) + assert len(res) == size + assert res[0] == 0 + assert res[5] == 5 + + def test_bigtest02(self): + '''Get String[]''' + + #req = sj.QueryRequest() + size = self.size + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'recids_str') + hm.put('size', str(size)) + params = sj.MapSolrParams(hm) + req = sj.LocalSolrQueryRequest(self.core, params) + + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('bigtest') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + + res = sj.JArray_string.cast_(message.getResults()) + assert len(res) == size + assert res[0] == '0' + assert res[5] == '5' + + + def test_bigtest03(self): + '''Get recids_hm_strstr''' + + #req = sj.QueryRequest() + size = self.size + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'recids_hm_strstr') + hm.put('size', str(size)) + params = sj.MapSolrParams(hm) + req = sj.LocalSolrQueryRequest(self.core, params) + + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('bigtest') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + + res = sj.HashMap.cast_(message.getResults()) + assert res.size() == size + assert str(sj.String.cast_(res.get('0'))) == '0' + assert str(sj.String.cast_(res.get('5'))) == '5' + + + def test_bigtest04(self): + '''Get recids_hm_strint''' + + #req = sj.QueryRequest() + size = self.size + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'recids_hm_strint') + hm.put('size', str(size)) + params = sj.MapSolrParams(hm) + req = sj.LocalSolrQueryRequest(self.core, params) + + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('bigtest') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + + res = sj.HashMap.cast_(message.getResults()) + assert res.size() == size + assert sj.Integer.cast_(res.get('0')).equals(0) + assert sj.Integer.cast_(res.get('5')).equals(5) + + + def test_bigtest05(self): + '''Get recids_hm_intint''' + + #req = sj.QueryRequest() + size = self.size + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'recids_hm_intint') + hm.put('size', str(size)) + params = sj.MapSolrParams(hm) + req = sj.LocalSolrQueryRequest(self.core, params) + + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('bigtest') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + + res = sj.HashMap.cast_(message.getResults()) + assert res.size() == size + assert sj.Integer.cast_(res.get(0)).equals(0) + assert sj.Integer.cast_(res.get(5)).equals(5) + + + def test_bigtest06(self): + '''Get recids_bitset - needs invenio.intbitset''' + + from invenio import intbitset + + #req = sj.QueryRequest() + size = self.size + hm = sj.HashMap().of_(sj.String, sj.String) + hm.put('action', 'recids_bitset') + hm.put('size', str(size)) + filled = int(size * 0.3) + hm.put('filled', str(filled)) + params = sj.MapSolrParams(hm) + req = sj.LocalSolrQueryRequest(self.core, params) + + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('bigtest') \ + .setSolrQueryResponse(rsp) \ + .setSolrQueryRequest(req) + + self.bridge.receive_message(message) + res = sj.JArray_byte.cast_(message.getResults()) + ibs = intbitset.intbitset() + ibs = ibs.fastload(res.string_) + + assert len(ibs) > 0 + + def timeit(self): + for x in range(1, 7): + testid = '%02d' % x + times = 10 + test_name = 'test_bigtest%s' % testid + if hasattr(self, test_name): + print test_name, + start = time.time() + test_method = getattr(self, test_name) + for x in xrange(times): + test_method() + end = time.time() - start + print end / times, 's.' + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.test_get_recids_changes4'] + unittest.main() diff --git a/test/python/unittest_invenio.py b/test/python/unittest_invenio.py new file mode 100644 index 000000000..d67b8ee2e --- /dev/null +++ b/test/python/unittest_invenio.py @@ -0,0 +1,232 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' +#@PydevCodeAnalysisIgnore + +import unittest +from montysolr import initvm, java_bridge, handler +import os + +sj = initvm.montysolr_java + +class TestHandler(handler.Handler): + def init(self): + #_b = os.path.join(os.path.dirname(__file__), 'testing_targets.py') + #self.discover_targets([_b]) + self.discover_targets(['montysolr.inveniopie.targets']) +test_handler = TestHandler() + +class Test(unittest.TestCase): + + + def setUp(self): + self.bridge = java_bridge.SimpleBridge(test_handler) + + def tearDown(self): + pass + + + def test_dict_cache(self): + + message = sj.PythonMessage('get_citation_dict') \ + .setSender('CitationQuery') \ + .setParam('dictname', 'citationdict') + self.bridge.receive_message(message) + + result = message.getParam('result') + print 'got result' + + + def test_workout_field_value(self): + + u = 'id:840017|arxiv_id:arXiv:0912.2620|src_dir:/Users/rca/work/indexing/fulltexts/arXiv' + message = sj.PythonMessage('workout_field_value') \ + .setSender('PythonTextField') \ + .setParam('externalVal', u) + self.bridge.receive_message(message) + + result = unicode(message.getParam('result')) + print len(result) + + def test_handle_request_body(self): + + req = sj.QueryRequest() + srp = sj.SolrQueryResponse() + + u = 'id:840017|arxiv_id:arXiv:0912.2620|src_dir:/Users/rca/work/indexing/fulltexts/arXiv' + message = sj.PythonMessage('handleRequestBody') \ + .setSender('rca.python.solr.handler.InvenioHandler') \ + .setParam('externalVal', u) + self.bridge.receive_message(message) + + result = unicode(message.getParam('result')) + print len(result) + + def test_format_search_results(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('format_search_results') \ + .setSender('InvenioFormatter') \ + .setSolrQueryResponse(rsp) \ + .setParam('recids', sj.JArray_int(range(0, 93))) + self.bridge.receive_message(message) + + result = unicode(rsp.getValues()) + assert 'inv_response' in result + assert '

' in result + + def test_get_recids_changes(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('get_recids_changes') \ + .setSender('InvenioKeepRecidUpdated') \ + .setSolrQueryResponse(rsp) \ + .setParam('last_recid', 30) + self.bridge.receive_message(message) + + results = message.getResults() + out = sj.HashMap.cast_(results) + + added = sj.JArray_int.cast_(out.get('ADDED')) + assert len(added) > 1 + + def test_get_recids_changes2(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('get_recids_changes') \ + .setSender('InvenioKeepRecidUpdated') \ + .setSolrQueryResponse(rsp) \ + .setParam('last_recid', 0) #test we can deal with extreme cases + self.bridge.receive_message(message) + + results = message.getResults() + out = sj.HashMap.cast_(results) + + added = sj.JArray_int.cast_(out.get('ADDED')) + assert len(added) > 1 + + def test_get_recids_changes3(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('get_recids_changes') \ + .setSender('InvenioKeepRecidUpdated') \ + .setSolrQueryResponse(rsp) \ + .setParam('last_recid', 9999999) + self.bridge.receive_message(message) + + results = message.getResults() + assert results is None + + + def test_get_recids_changes4(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('get_recids_changes') \ + .setSender('InvenioKeepRecidUpdated') \ + .setSolrQueryResponse(rsp) \ + .setParam('last_recid', -1) + self.bridge.receive_message(message) + + results = message.getResults() + out = sj.HashMap.cast_(results) + + added = sj.JArray_int.cast_(out.get('ADDED')) + updated = sj.JArray_int.cast_(out.get('CHANGED')) + deleted = sj.JArray_int.cast_(out.get('DELETED')) + + + assert len(added) == 104 + assert len(updated) == 0 + assert len(deleted) == 0 + + + def test_perform_request_search_ints(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('perform_request_search_ints') \ + .setSender('InvenioQuery') \ + .setSolrQueryResponse(rsp) \ + .setParam('query', 'ellis') + self.bridge.receive_message(message) + + results = message.getResults() + out = sj.JArray_int.cast_(results) + + assert len(out) > 1 + + def test_sort_and_format(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + kwargs = sj.HashMap() + kwargs.put("of", "hcs") + #kwargs.put("sf", "year") #sort by year + kwargs.put('colls_to_search', """['Articles & Preprints', 'Multimedia & Arts', 'Books & Reports']""") + + message = sj.PythonMessage('sort_and_format') \ + .setSender('InvenioFormatter') \ + .setSolrQueryResponse(rsp) \ + .setParam('recids', sj.JArray_int(range(0, 93))) \ + .setParam("kwargs", kwargs) + + self.bridge.receive_message(message) + + result = unicode(message.getResults()) + assert '

' in result + + def test_sort_and_format2(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + kwargs = sj.HashMap() + #kwargs.put("of", "hcs") + kwargs.put("sf", "year") #sort by year + kwargs.put('colls_to_search', """['Articles & Preprints', 'Multimedia & Arts', 'Books & Reports']""") + + message = sj.PythonMessage('sort_and_format') \ + .setSender('InvenioFormatter') \ + .setSolrQueryResponse(rsp) \ + .setParam('recids', sj.JArray_int(range(0, 93))) \ + .setParam("kwargs", kwargs) + + self.bridge.receive_message(message) + + result = sj.JArray_int.cast_(message.getResults()) + assert len(result) > 3 + assert result[0] == 77 + + def test_diagnostic_test(self): + + req = sj.QueryRequest() + rsp = sj.SolrQueryResponse() + + message = sj.PythonMessage('diagnostic_test') \ + .setSolrQueryResponse(rsp) \ + .setParam('recids', sj.JArray_int(range(0, 93))) + + self.bridge.receive_message(message) + + res = message.getResults() + print res + + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.test_get_recids_changes4'] + unittest.main() diff --git a/test/python/unittest_python_bridge.py b/test/python/unittest_python_bridge.py new file mode 100644 index 000000000..e74861701 --- /dev/null +++ b/test/python/unittest_python_bridge.py @@ -0,0 +1,70 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' +import unittest +from montysolr import handler +from montysolr.python_bridge import JVMBridge +from montysolr.utils import MontySolrTarget +import sys +import os + +sj = JVMBridge.getObjMontySolr() + +class TestingMethods(): + def montysolr_targets(self): + + def test_a(message): + data = sj.JArray_int.cast_(message.getParam("data")) + data = data * 2 + message.setParam("result", sj.JArray_int(data)) + + def test_b(message): + data = sj.JArray_int.cast_(message.getParam("data")) + data = str(data) + message.setParam("result", data) + + return [ + MontySolrTarget(':test_a', test_a), + MontySolrTarget(':test_b', test_b), + ] + +class TestHandler(handler.Handler): + def init(self): + self.discover_targets([TestingMethods()]) + +class Test(unittest.TestCase): + + def setUp(self): + self._handler = JVMBridge._handler + JVMBridge.setHandler(TestHandler()) + + def tearDown(self): + JVMBridge.setHandler(self._handler) + + def test_basic(self): + + sj = JVMBridge.getObjMontySolr() + message = JVMBridge.createMessage("test_a") \ + .setParam('data', sj.JArray_int([0,1,2])) + + JVMBridge.sendMessage(message) + res = list(sj.JArray_int.cast_(message.getParam("result"))) + assert res == [0, 1, 2, 0, 1, 2] + + #lets reuse the message object + message.setReceiver("test_b") + JVMBridge.sendMessage(message) + res = str(message.getParam("result")) + assert res.find("[0, 1, 2]") > -1 + + message = JVMBridge.createMessage("test_b") \ + .setParam('data', sj.JArray_int([0,1,2])) + JVMBridge.sendMessage(message) + res = str(message.getParam("result")) + assert res.find("[0, 1, 2]") > -1 + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/python/unittest_solr.py b/test/python/unittest_solr.py new file mode 100644 index 000000000..a40a27ff7 --- /dev/null +++ b/test/python/unittest_solr.py @@ -0,0 +1,62 @@ +''' +Created on Feb 4, 2011 + +@author: rca +''' + +import os +# "-Djava.util.logging.config.file=/x/dev/workspace/sandbox/montysolr/example/etc/test.logging.properties" +os.environ['MONTYSOLR_JVMARGS_PYTHON'] = "" + +import unittest +from montysolr import initvm + + +sj = initvm.montysolr_java +solr = initvm.solr_java +lu = initvm.lucene + + +class Test(unittest.TestCase): + + + def setUp(self): + + self.initializer = sj.CoreContainer.Initializer() + self.conf = {'solr_home': '/x/dev/workspace/sandbox/montysolr/example/solr', + 'data_dir': '/x/dev/workspace/sandbox/montysolr/example/solr/data-jtest'} + + sj.System.setProperty('solr.solr.home', self.conf['solr_home']) + sj.System.setProperty('solr.data.dir', self.conf['data_dir']) + self.core_container = self.initializer.initialize() + self.server = sj.EmbeddedSolrServer(self.core_container, "") + + solr_config = sj.SolrConfig() + index_schema = sj.IndexSchema(solr_config, None, None) + q = sj.QueryParsing.parseQuery('*:*', index_schema) + + def tearDown(self): + self.core_container.shutdown() + + + def test_solr_all(self): + + server = self.server + + # create a query + query = sj.SolrQuery() + query.setQuery('*:*') + + query_response = server.query(query) + + head_part = query_response.getResponseHeader() + res_part = query_response.getResults() + qtime = query_response.getQTime() + etime = query_response.getElapsedTime() + + print qtime, etime, head_part, res_part + + +if __name__ == "__main__": + #import sys;sys.argv = ['', 'Test.testName'] + unittest.main() \ No newline at end of file diff --git a/test/test-files/README b/test/test-files/README new file mode 100644 index 000000000..10f878acc --- /dev/null +++ b/test/test-files/README @@ -0,0 +1,21 @@ + + +This directory is where any non-transient, non-java files needed +for the execution of tests should live. + +It is used as the CWD when running JUnit tests. diff --git a/test/test-files/invenio-test-queries.result b/test/test-files/invenio-test-queries.result new file mode 100644 index 000000000..1dbb9c954 --- /dev/null +++ b/test/test-files/invenio-test-queries.result @@ -0,0 +1,290 @@ +reportnumber:arxiv:0711.2908 or arxiv:0705.4298 or reportnumber:hep-ph/0504227 + +OK invenio=3 montysolr=3 +--- +reference:Phys.Rev.Lett.,28,1421 or reference:arXiv:0711.4556 + +OK invenio=457 montysolr=457 +--- +author:hawking and affiliation:"cambridge u., damtp" and year:2004->9999 ++ + +9999> +OK invenio=10 montysolr=10 +--- +hey |muon + +OK invenio=29520 montysolr=29520 +--- +hey |"muon muon" + <"muon muon"> +OK invenio=599 montysolr=599 +--- +"and or not AND OR NOT" and phrase ++<"and or not and or not"> + +OK invenio=0 montysolr=0 +--- +author:hawking and affiliation:"cambridge u., damtp" and year:2004->9999 ++ + +9999> +OK invenio=10 montysolr=10 +--- +thomas crewther quark 2002 ++ + + +<2002> +OK invenio=2 montysolr=2 +--- +journal:phys.rev.lett.,62,1825 + +OK invenio=1 montysolr=1 +--- +journal:"Phys.Rev.Lett.,105*" or journal:Phys.Lett. and author:thomas + + + +NO invenio=1134 montysolr=1114 +--- +year:1997-11-18 + +OK invenio=0 montysolr=0 +--- +datecreated:2011-01-26 and title:neutrino* ++ + +OK invenio=0 montysolr=0 +--- +author:unruh or title:cauchy not title:problem and 037__c:gr-qc + - +<037__c|gr-qc> +NO invenio=79 montysolr=23839 +--- +(author:"albrow, m*") and journal:phys.rev.lett. and (title:quark* and title:cited:200->99999) ++ + +(+ +99999>) +OK invenio=2 montysolr=2 +--- +reference:"Phys.Rev.Lett.,*" ++ + +NO invenio=280780 montysolr=0 +--- +citedby:hep-th/9711200 author:cvetic ++ + +OK invenio=2 montysolr=2 +--- +author:parke citedby:author:witten ++ + +OK invenio=4 montysolr=4 +--- +refersto:hep-th/9711200 title:nucl* ++ + +OK invenio=28 montysolr=28 +--- +author:witten refersto:author:"parke, s j" ++ + +OK invenio=6 montysolr=6 +--- +refersto:author:parke or refersto:author:lykken author:witten + + +NO invenio=11 montysolr=434 +--- +affiliation:"oxford u." refersto:title:muon* ++ + +OK invenio=1030 montysolr=1030 +--- +affiliation:"harvard u." + +OK invenio=7270 montysolr=7270 +--- +"Ellis, J" +<"ellis, j"> +OK invenio=0 montysolr=0 +--- +'muon decay' +<'muon decay'> +OK invenio=563 montysolr=563 +--- +'Ellis, J' +<'ellis, j'> +OK invenio=936 montysolr=936 +--- +ellis +muon ++ + +OK invenio=217 montysolr=217 +--- +ellis muon ++ + +OK invenio=217 montysolr=217 +--- +ellis and muon ++ + +OK invenio=217 montysolr=217 +--- +ellis -muon ++ - +OK invenio=2263 montysolr=2263 +--- +ellis not muon ++ - +OK invenio=2263 montysolr=2263 +--- +ellis |muon + +OK invenio=31216 montysolr=31216 +--- +ellis or muon + +OK invenio=31216 montysolr=31216 +--- +muon or kaon and ellis + + + +NO invenio=240 montysolr=30 +--- +ellis and muon or kaon ++ +NO invenio=6191 montysolr=2480 +--- +muon or kaon and ellis -decay + + + - +NO invenio=240 montysolr=30 +--- +(gravity OR supergravity) AND (ellis OR perelstein) ++( ) +( ) +OK invenio=201 montysolr=201 +--- +C++ + +OK invenio=226 montysolr=226 +--- +O'Shea + +OK invenio=550 montysolr=550 +--- +$e^{+}e^{-}$ +<$e^{+}e^{-}$> +OK invenio=124 montysolr=124 +--- +hep-ph/0204133 + +OK invenio=1 montysolr=1 +--- +BlaCK hOlEs ++ + +OK invenio=26165 montysolr=26165 +--- +пушкин +<пушкин> +OK invenio=0 montysolr=0 +--- +muon* + +OK invenio=29811 montysolr=29811 +--- +CERN-TH*31 + +OK invenio=62 montysolr=62 +--- +a* + +OK invenio=510300 montysolr=510300 +--- +"Neutrino mass*" +<"neutrino mass*"> +OK invenio=1217 montysolr=1217 +--- +author:ellis + +OK invenio=2234 montysolr=2234 +--- +author:ellis title:muon* ++ + +OK invenio=44 montysolr=44 +--- +experiment:NA60 year:2001 ++ + +OK invenio=0 montysolr=0 +--- +title:/^E.*s$/ + +OK invenio=16410 montysolr=16410 +--- +author:/^Ellis, (J|John)$/ + +NO invenio=1018 montysolr=0 +--- +title:/dense ([^ l]* )?matter/ + +OK invenio=0 montysolr=0 +--- +collection:PREPRINT -year:/^[0-9]{4}([\?\-]|\-[0-9]{4})?$/ ++ - +OK invenio=0 montysolr=0 +--- +collection:PREPRINT -year:/^[[:digit:]]{4}([\?\-]|\-[[:digit:]]{4})?$/ ++ - +OK invenio=0 montysolr=0 +--- +muon decay year:1983->1992 ++ + +1992> +OK invenio=0 montysolr=0 +--- +author:"Ellis, J"->"Ellis, Qqq" ++ -<>> +<"ellis, qqq"> +NO invenio=1437 montysolr=0 +--- +refersto:reportnumber:hep-th/0201100 + +OK invenio=34 montysolr=34 +--- +citedby:author:klebanov + +OK invenio=2022 montysolr=2022 +--- +refersto:author:"Klebanov, I" + +OK invenio=9831 montysolr=9831 +--- +refersto:keyword:gravitino + +OK invenio=17014 montysolr=17014 +--- +author:klebanov AND citedby:author:papadimitriou NOT refersto:author:papadimitriou ++ + - +OK invenio=10 montysolr=10 +--- +refersto:/author:"Klebanov, I" title:O(N)/ ++ + + + +NO invenio=119 montysolr=0 +--- +author:ellis -muon* +abstract:'dense quark matter' year:200* ++ - + + +OK invenio=2 montysolr=2 +--- +author:ellis -muon* +title:'dense quark matter' year:200* ++ - + + +OK invenio=1 montysolr=1 +--- +higgs or reference:higgs or fulltext:higgs + fulltext:higgs +NO invenio=37625 montysolr=61090 +--- +author:lin fulltext:Schwarzschild fulltext:AdS reference:"Adv. Theor. Math. Phys." ++ +fulltext:schwarzschild +fulltext:ads + +<"adv. theor. math. phys."> +OK invenio=0 montysolr=0 +--- +author:/^Ellis, (J|John)$/ + +NO invenio=1018 montysolr=0 +--- +fulltext:e- +fulltext:e- +NO invenio=854 montysolr=1187 +--- +muon or fulltext:muon and author:ellis + +fulltext:muon + +NO invenio=267 montysolr=0 +--- +reference:hep-ph/0103062 + +OK invenio=341 montysolr=341 +--- +reference:giddings reference:ross reference:"Phys. Rev., D" reference:61 reference:2000 ++ + + +<"phys. rev., d"> + + +OK invenio=0 montysolr=0 +--- +standard model -author:ellis reference:ellis ++ + - + +OK invenio=0 montysolr=0 + +total=72, success/mismatch/error=58/14/0 + diff --git a/test/test-files/invenio-test-queries.txt b/test/test-files/invenio-test-queries.txt new file mode 100644 index 000000000..e9442a1e1 --- /dev/null +++ b/test/test-files/invenio-test-queries.txt @@ -0,0 +1,73 @@ +reportnumber:arxiv:0711.2908 or arxiv:0705.4298 or reportnumber:hep-ph/0504227 +reference:Phys.Rev.Lett.,28,1421 or reference:arXiv:0711.4556 +author:hawking and affiliation:"cambridge u., damtp" and year:2004->9999 +hey |muon +hey |"muon muon" +"and or not AND OR NOT" and phrase +author:hawking and affiliation:"cambridge u., damtp" and year:2004->9999 +thomas crewther quark 2002 +journal:phys.rev.lett.,62,1825 +journal:"Phys.Rev.Lett.,105*" or journal:Phys.Lett. and author:thomas +year:1997-11-18 +datecreated:2011-01-26 and title:neutrino* +author:unruh or title:cauchy not title:problem and 037__c:gr-qc +(author:"albrow, m*") and journal:phys.rev.lett. and (title:quark* and title:cited:200->99999) +reference:"Phys.Rev.Lett.,*" +citedby:hep-th/9711200 author:cvetic +author:parke citedby:author:witten +refersto:hep-th/9711200 title:nucl* +author:witten refersto:author:"parke, s j" +refersto:author:parke or refersto:author:lykken author:witten +affiliation:"oxford u." refersto:title:muon* +affiliation:"harvard u." +"Ellis, J" +'muon decay' +'Ellis, J' +ellis +muon +ellis muon +ellis and muon +ellis -muon +ellis not muon +ellis |muon +ellis or muon +muon or kaon and ellis +ellis and muon or kaon +muon or kaon and ellis -decay +(gravity OR supergravity) AND (ellis OR perelstein) +C++ +O'Shea +$e^{+}e^{-}$ +hep-ph/0204133 +BlaCK hOlEs +пушкин +muon* +CERN-TH*31 +a* +"Neutrino mass*" +author:ellis +author:ellis title:muon* +experiment:NA60 year:2001 +title:/^E.*s$/ +author:/^Ellis, (J|John)$/ +title:/dense ([^ l]* )?matter/ +collection:PREPRINT -year:/^[0-9]{4}([\?\-]|\-[0-9]{4})?$/ +collection:PREPRINT -year:/^[[:digit:]]{4}([\?\-]|\-[[:digit:]]{4})?$/ +muon decay year:1983->1992 +author:"Ellis, J"->"Ellis, Qqq" +refersto:reportnumber:hep-th/0201100 +citedby:author:klebanov +refersto:author:"Klebanov, I" +refersto:keyword:gravitino +author:klebanov AND citedby:author:papadimitriou NOT refersto:author:papadimitriou +refersto:/author:"Klebanov, I" title:O(N)/ +author:ellis -muon* +abstract:'dense quark matter' year:200* +author:ellis -muon* +title:'dense quark matter' year:200* +higgs or reference:higgs or fulltext:higgs +author:lin fulltext:Schwarzschild fulltext:AdS reference:"Adv. Theor. Math. Phys." +author:/^Ellis, (J|John)$/ +fulltext:e- +muon or fulltext:muon and author:ellis +reference:hep-ph/0103062 +reference:giddings reference:ross reference:"Phys. Rev., D" reference:61 reference:2000 +standard model -author:ellis reference:ellis + diff --git a/test/test-files/solr/conf/data-config-test-java.xml b/test/test-files/solr/conf/data-config-test-java.xml new file mode 100644 index 000000000..3dec8773e --- /dev/null +++ b/test/test-files/solr/conf/data-config-test-java.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test-files/solr/conf/data-config.xml b/test/test-files/solr/conf/data-config.xml new file mode 100644 index 000000000..3a0080d5a --- /dev/null +++ b/test/test-files/solr/conf/data-config.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test-files/solr/conf/elevate.xml b/test/test-files/solr/conf/elevate.xml new file mode 100755 index 000000000..9b4caec69 --- /dev/null +++ b/test/test-files/solr/conf/elevate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/test-files/solr/conf/protwords.txt b/test/test-files/solr/conf/protwords.txt new file mode 100755 index 000000000..1dfc0abec --- /dev/null +++ b/test/test-files/solr/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/test/test-files/solr/conf/schema.xml b/test/test-files/solr/conf/schema.xml new file mode 100755 index 000000000..3b63345be --- /dev/null +++ b/test/test-files/solr/conf/schema.xml @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test-files/solr/conf/solrconfig.xml b/test/test-files/solr/conf/solrconfig.xml new file mode 100755 index 000000000..c9e597df1 --- /dev/null +++ b/test/test-files/solr/conf/solrconfig.xml @@ -0,0 +1,1115 @@ + + + + + + ${solr.abortOnConfigurationError:true} + + + + + + + + + + + + + + + + ${solr.data.dir:./solr/data} + + + + + + false + + 10 + + + + + 32 + + 10000 + 1000 + 10000 + + + + + + + + + + + + + native + + + + + + + false + 32 + 10 + + + + + + + + false + + + true + + + + + + + + 1 + + 0 + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + true + + + + + + + + 20 + + + 200 + + + + + + + + + + + + + solr rocks010 + static firstSearcher warming query from solrconfig.xml + {!iq}inv_refersto:"recid:100"010invenio + {!iq}inv_citedby:"recid:100"010invenio + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + explicit + false + + + + + + + + explicit + 10000 + + + + + + query + invenio-formatter + facet + mlt + highlight + stats + debug + + + + explicit + + + + + + + + + + + + + dismax + explicit + 0.01 + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + + + text^0.2 features^1.1 name^1.5 manu^1.4 manu_exact^1.9 + + + popularity^0.5 recip(price,1,1000,1000)^0.3 + + + id,name,price,score + + + 2<-1 5<-2 6<90% + + 100 + *:* + + text features name + + 0 + + name + regex + + + + + + + dismax + explicit + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 + 2<-1 5<-2 6<90% + + incubationdate_dt:[* TO NOW/DAY-1MONTH]^2.2 + + + + inStock:true + + + + cat + manu_exact + price:[* TO 500] + price:[500 TO *] + + + + + + + + + + textSpell + + + default + name + ./spellchecker + + + + + + + + + + + + + + + + false + + false + + 1 + + + spellcheck + + + + + + + + true + + + tvComponent + + + + + + + + + default + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + 20 + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + true + default + true + + name + id + + features + + true + + + + false + + + clusteringComponent + + + + + + + + text + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + true + + + termsComponent + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + standard + solrpingquery + all + + + + + + + explicit + true + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + solr + + + + + + + + + data-config.xml + false + false + + + + + data-config.xml + false + false + + + + + data-config-test-java.xml + false + false + + + + + + last_modified + ignored_ + + + + + diff --git a/test/test-files/solr/conf/spellings.txt b/test/test-files/solr/conf/spellings.txt new file mode 100755 index 000000000..d7ede6f56 --- /dev/null +++ b/test/test-files/solr/conf/spellings.txt @@ -0,0 +1,2 @@ +pizza +history \ No newline at end of file diff --git a/test/test-files/solr/conf/stopwords.txt b/test/test-files/solr/conf/stopwords.txt new file mode 100755 index 000000000..b5824da32 --- /dev/null +++ b/test/test-files/solr/conf/stopwords.txt @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +#Standard english stop words taken from Lucene's StopAnalyzer +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with + diff --git a/test/test-files/solr/conf/synonyms.txt b/test/test-files/solr/conf/synonyms.txt new file mode 100755 index 000000000..b0e31cb7e --- /dev/null +++ b/test/test-files/solr/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma +