diff --git a/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt b/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt new file mode 100644 index 00000000..9b0bcfe2 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/CHANGELOG.txt @@ -0,0 +1,178 @@ +Search API Solr search 1.4 (12/25/2013): +---------------------------------------- +- #2157839 by drunken monkey, Nick_vh: Updated config files to the newest + version. +- #2130827 by drunken monkey: Added additional Solr server information to the + Server overview. +- #2126281 by drunken monkey: Update error handling according to the latest + Search API change. +- #2127991 by drunken monkey: Fixed handling of negated fulltext keys. +- #2113943 by drunken monkey: Fixed clash in specifying the HTTP method for + searches. +- #2127193 by jlapp: Fixed date field values returned for multi-index searches. +- #2122155 drunken monkey: Added the "Files" tab to contextual links. +- #1846860 by andrewbelcher, drclaw, drunken monkey, danielnolde: Added a way + to easily define new dynamic field types. +- #2064377 by Nick_vh: Made configuration files compatible with Solr Cloud. +- #2107417 by Nick_vh: Fixed config files for Solr 4.5. + +Search API Solr search 1.3 (10/23/2013): +---------------------------------------- +- #2099683 by drunken monkey: Added support for 'virtual fields' in Views. +- #1997702 by ianthomas_uk, drunken monkey: Added "AUTO" mode for HTTP method. +- #2033913 by drunken monkey: Fixed small error in schema.xml. +- #2073441 by drunken monkey: Removed custom uninstall code for deleting + dependent servers. +- #1882190 by corvus_ch, arnested, drunken monkey: Added optional index ID + prefixes. + +Search API Solr search 1.2 (09/01/2013): +---------------------------------------- +- #1246730 by febbraro, maciej.zgadzaj, drunken monkey: Added a way to alter + the Solr document when indexing. +- #2053553 by drunken monkey, andrewbelcher: Fixed spatial features with clean + field identifiers. +- #2054373 by drunken monkey: Added the option to use clean field identifiers. +- #1992806 by drunken monkey: Documented problems with Solr 4.3+. +- #2045355 by drunken monkey, arpieb: Fixed result mapping of item IDs. +- #2050157 by izus: Fixed typo in stopwords.txt. + +Search API Solr search 1.1 (07/21/2013): +---------------------------------------- +- #1957730 by drunken monkey: Fixed filter query strings for negated filters. +- #2010818 by kenorb, drunken monkey: Added new Files tab showing all used solr + config files. +- #2042201 by klausi: Fixed timeouts while optimizing Solr server. +- #2034719 by fago: Added raw term to autocompletion response. +- #2027843 by fago, drunken monkey: Made the Solr response available as part of + the search results. +- #1834614 by drunken monkey: Fixed date fields in MLT queries. +- #1970652 by jsteggink: Fixed highlighting for text fields. +- #2016169 by tomdearden, drunken monkey: Fixed parsing of facets on + multi-valued fields. +- #2008034 by bdecarne: Fixed highlighting in multi-index searches. + +Search API Solr search 1.0 (06/09/2013): +---------------------------------------- +- #1896080 by drunken monkey: Included additional required config files in the + module. +- #1919572 by chaby: Fixed indexing of geohashes. +- #2004596 by drunken monkey: Fixed "More Like This" for Solr 4.x. +- #2007214 by drunken monkey: Fixed unsetting of object properties. +- #1884312 by drunken monkey, mvc: Fixed resetting of HTTP password upon + re-saving of the configuration form. +- #1957774 by drunken monkey: Fixed displayed link to local Solr servers. +- #1721262 by Steven Jones, das-peter, drunken monkey: Added field collapsing + support. +- #1549244 by cferthorney, drunken monkey: Added SSL Support for Solr servers. + +Search API Solr search 1.0, RC 5 (05/17/2013): +---------------------------------------------- +- #1190462 by drunken monkey: Documented that enabling HTML filter makes sense. +- #1986284 by drunken monkey: Updated common configs to the latest version. +- #1990422 by populist, drunken monkey: Added support for custom stream contexts + for HTTP requests. +- #1957890 by drunken monkey, jwilson3: Fixed several bugs for facets. +- #1676224 by dasjo, morningtime, drunken monkey: Added support for Solr 4.x. +- #1985522 by chaby: Fixed use of instance method in static escape() method. +- #1979102 by drunken monkey: Fixed wrong limit for limit-less searches. +- #1978632 by chaby, drunken monkey: Fixed wrong check on softCommit. +- #1978600 by chaby: Fixed hook_requirements() for install phase. +- #1976930 by drunken monkey: Fixed duplicate method in SearchApiSolrField. + +Search API Solr search 1.0, RC 4 (04/22/2013): +---------------------------------------------- +- #1744250 by mollux, drunken monkey, das-peter: Added support for + location-based searches. +- #1846254 by drunken monkey: Removed the SolrPhpClient dependency. +- #1934450 by jwilson3, jlapp: Fixed reference to removed method + getFacetField(). +- #1900644 by Deciphered: Fixed facet handling for multi-index searches. +- #1897386 by drunken monkey, NIck_vh: Update the common schema. + +Search API Solr search 1.0, RC 3 (01/06/2013): +---------------------------------------------- +- #1828260 by drunken monkey: Fixed filtering by index in multi-index searches. +- #1509380 by drunken monkey: Adopt common config files. +- #1815348 by drunken monkey: Fixed queryMultiple() to not use item ID as the + array key. +- #1789204 by Steven Jones: Added way to easily alter the fl parameter. +- #1744250 by mollux, dasjo: Added support for location based search. +- #1813670 by guillaumev: Fixed check for autocomplete configuration in form. +- #1425910 by drunken monkey, mh86: Added setting for maximum occurence + threshold in autocomplete. +- #1691132 by drunken monkey, David Stosik: Fixed calls to watchdog(). +- #1588130 by regilero, David Stosik, drunken monkey: Fixed error handling. +- #1805720 by drunken monkey: Added additional options and improvements for the + autocomplete functionality. +- #1276970 by derhasi, moonray: Fixed large queries break Solr search. +- #1299940 by drunken monkey: Fixed handling of empty response. +- #1507818 by larowlan: Fixed field boosts for standard request handler. + +Search API Solr search 1.0, RC 2 (05/23/2012): +---------------------------------------------- +- Fixed escaping of error messages. +- #1480170 by kotnik: Fixed return value of hook_requirements(). +- #1500210 by ezra-g, acrollet, jsacksick: Fixed errors when installing with + non-default installation profiles. +- #1444432 by Damien Tournoud, jsacksick: Added field-level boosting. +- #1302406 by Steven Jones: Fixed autoload problem during installation. +- #1340244 by drunken monkey, alanomaly: Added more helpful error messages. + +Search API Solr search 1.0, RC 1 (11/10/2011): +---------------------------------------------- +- #1308638 by drunken monkey: Adapted to new structure of field settings. +- #1308498 by zenlan, drunken monkey: Added flexibility for facet fields. +- #1319544 by drunken monkey: Fixed never delete contents of read-only indexes. +- #1309650 by jonhattan, drunken monkey: Added support for the Libraries API. + +Search API Solr search 1.0, Beta 4 (09/08/2011): +------------------------------------------------ +- #1230536 by thegreat, drunken monkey: Added support for OR facets. +- #1184002 by drunken monkey: Fixed support of the latest SolrPhpClient version. +- #1032848 by das-peter, drunken monkey: Added possibility to save SolrPhpClient + to the libraries directory. +- #1225926 by drunken monkey, fago: Fixed performance problems in indexing + workflow. +- #1219310 by drunken monkey: Adapted to recent API change. +- #1203680 by klausi: Fixed use of taxonomy terms for "More like this". +- #1181260 by klausi: Fixed mlt.maxwl in solrconfig.xml. +- #1116896 by drunken monkey: Adapted to newer Solr versions. +- #1190462 by drunken monkey: Added option to directly highlight retrieved data + from Solr. +- #1196514 by drunken monkey, klausi: Fixed case sensitivity of input keys for + autocomplete. +- #1192654 by drunken monkey: Added support for the Autocomplete module. +- #1177648 by drunken monkey: Added option to use Solr's built-in highlighting. +- #1154116 by drunken monkey: Added option for retrieving search results data + directly from Solr. +- #1184002 by drunken monkey: Fixed INSTALL.txt to reflect that the module + doesn't work with the latest Solr PHP Client version. + +Search API Solr search 1.0, Beta 3 (06/06/2011): +------------------------------------------------ +- #1111852 by miiimooo, drunken monkey: Added a 'More like this' feature. +- #1153306 by JoeMcGuire, drunken monkey: Added spellchecking support. +- #1138230 by becw, drunken monkey: Added increased flexibility to the service + class. +- #1127038 by drunken monkey: Fixed handling of date facets. +- #1110820 by becw, drunken monkey: Added support for the Luke request handler. +- #1095956 by drunken monkey: Added Solr-specific index alter hook. + +Search API Solr search 1.0, Beta 2 (03/04/2011): +------------------------------------------------ +- #1071894 by drunken monkey: Fixed incorrect handling of boolean facets. +- #1071796: Add additional help for Solr-specific extensions. +- #1056018: Better document Solr config customization options. +- #1049900: Field values are sometimes not escaped properly. +- #1043586: Allow Solr server URL to be altered. +- #1010610 by mikejoconnor: Fix hook_requirements(). +- #1024146: Don't use file_get_contents() for contacting the Solr server. +- #1010610: More helpful error message when SolrPhpClient is missing. +- #915174: Remove unnecessary files[] declarations from .info file. +- #984134: Add Solr-specific query alter hooks. + +Search API Solr search 1.0, Beta 1 (11/29/2010): +------------------------------------------------ +Basic functionality is in place and quite well-tested, including support for +facets and for multi-index searches. diff --git a/sites/all/modules/contrib/search/search_api_solr/INSTALL.txt b/sites/all/modules/contrib/search/search_api_solr/INSTALL.txt new file mode 100644 index 00000000..d8c1827a --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/INSTALL.txt @@ -0,0 +1,79 @@ +Setting up Solr +--------------- + +In order for this module to work, you will first need to set up a Solr server. +For this, you can either purchase a server from a web Solr hosts or set up your +own Solr server on your web server (if you have the necessary rights to do so). +If you want to use a hosted solution, a number of companies are listed on the +module's project page [1]. Otherwise, please follow the instructions below. +A more detailed set of instructions is available at [2]. + +[1] https://drupal.org/project/search_api_solr +[2] https://drupal.org/node/1999310 + +As a pre-requisite for running your own Solr server, you'll need Java 6 or +higher. + +Download the latest version of Solr 4.x from [3] and unpack the archive +somewhere outside of your web server's document tree. + +[3] http://www.apache.org/dyn/closer.cgi/lucene/solr/ + +This module also supports Solr 1.4 and 3.x. For better performance and more +features, 4.x should be used, though. 1.4 is discouraged altogether, as several +features of the module don't work at all in 1.4. + +For small websites, using the example application, located in $SOLR/example/, +usually suffices. In any case, you can use it for developing andd testing. The +following instructions will assume you are using the example application, +otherwise you should be able to substitute the corresponding paths. + +NOTE: The Solr 4.3+ example application is currently not completely supported +with the configuration files included in this module, due to a slight change in +directory structure. To fix this, simply copy, move or symlink the contrib/ +directory from the top level of the extracted Solr package one level down to +example/. +(For other directory structures: the contrib/ directory has to be in the +directory two levels up from the one which includes the conf/ directory. For +help, just start the Solr server and check the log files for WARN messages – +they should state in which place Solr expects the directory to be.) + +CAUTION! For production sites, it is vital that you somehow prevent outside +access to the Solr server. Otherwise, attackers could read, corrupt or delete +all your indexed data. Using the example server WON'T prevent this by default. +If it is available, the probably easiest way of preventing this is to disable +outside access to the ports used by Solr through your server's network +configuration or through the use of a firewall. +Other options include adding basic HTTP authentication or renaming the solr/ +directory to a random string of characters and using that as the path. + +Before starting the Solr server you will have to make sure it uses the proper +configuration files. These are located in the solr-conf/ directory in this +module, in a sub-directory according to the Solr version you are using. Copy all +the files from that directory into Solr's configuration directory +($SOLR/example/solr/collection1/conf/ in case of the 4.x example application), +after backing up all files that would be overwritten. + +NOTE: The mapping-ISOLatin1Accent.txt is only included in the module for +completeness' sake, as it is required to start the Solr server. It will be +usually advisable to just use the file of the example application in this case, +though, as it contains really useful definitions, while the file provided with +this module is empty, apart from some documentation. For licensing reasons, it +is not possible for us to include the definitions in the example config file in +the copy this module provides. + +You can then start Solr. For the example application, go to $SOLR/example/ and +issue the following command (assuming Java is correctly installed): + +java -jar start.jar & + +Afterwards, go to [4] in your web browser to ensure Solr is running correctly. + +[4] http://localhost:8983/solr/#/ + +You can then enable this module and create a new server, using the "Solr search" +service class. Enter the hostname, port and path corresponding to your Solr +server in the appropriate fields. The default values already correspond to the +example application, so you won't have to change the values if you use that. +If you are using HTTP Authentication to protect your Solr server you also have +to provide the appropriate user and password here. diff --git a/sites/all/modules/contrib/search/search_api_solr/LICENSE.txt b/sites/all/modules/contrib/search/search_api_solr/LICENSE.txt new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser General +Public License instead of this License. diff --git a/sites/all/modules/contrib/search/search_api_solr/README.txt b/sites/all/modules/contrib/search/search_api_solr/README.txt new file mode 100644 index 00000000..8c1443a1 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/README.txt @@ -0,0 +1,155 @@ +Solr search +----------- + +This module provides an implementation of the Search API which uses an Apache +Solr search server for indexing and searching. Before enabling or using this +module, you'll have to follow the instructions given in INSTALL.txt first. + +For more detailed documentation, see the handbook [1]. + +[1] https://drupal.org/node/1999280 + +Supported optional features +--------------------------- + +All Search API datatypes are supported by using appropriate Solr datatypes for +indexing them. By default, "String"/"URI" and "Integer"/"Duration" are defined +equivalently. However, through manual configuration of the used schema.xml this +can be changed arbitrarily. Using your own Solr extensions is thereby also +possible. + +The "direct" parse mode for queries will result in the keys being directly used +as the query to Solr. For details about Lucene's query syntax, see [2]. There +are also some Solr additions to this, listed at [3]. Note however that, by +default, this module uses the dismax query handler, so searches like +"field:value" won't work with the "direct" mode. + +[2] http://lucene.apache.org/java/2_9_1/queryparsersyntax.html +[3] http://wiki.apache.org/solr/SolrQuerySyntax + +Regarding third-party features, the following are supported: + +- search_api_autocomplete + Introduced by module: search_api_autocomplete + Lets you add autocompletion capabilities to search forms on the site. (See + also "Hidden variables" below for Solr-specific customization.) +- search_api_facets + Introduced by module: search_api_facetapi + Allows you to create facetted searches for dynamically filtering search + results. +- search_api_facets_operator_or + Introduced by module: search_api_facetapi + Allows the creation of OR facets. +- search_api_mlt + Introduced by module: search_api_views + Lets you display items that are similar to a given one. Use, e.g., to create + a "More like this" block for node pages. + NOTE: Due to a regression in Solr itself, "More like this" doesn't work with + integer and float fields in Solr 4. As a work-around, you can index the fields + (or copies of them) as string values. See [4] for details. + Also, MLT with date fields isn't currently supported at all for any version. +- search_api_multi + Introduced by module: search_api_multi + Allows you to search multiple indexes at once, as long as they are on the same + server. You can use this to let users simultaneously search all content on the + site – nodes, comments, user profiles, etc. +- search_api_spellcheck + Introduced by module: search_api_spellcheck + Gives the option to display automatic spellchecking for searches. +- search_api_data_type_location + Introduced by module: search_api_location + Lets you index, filter and sort on location fields. Note, however, that only + single-valued fields are currently supported for Solr 3.x, and that the option + isn't supported at all in Solr 1.4. +- search_api_grouping + Introduced by module: search_api_grouping [5] + Lets you group search results based on indexed fields. For further information + see the FieldCollapsing documentation in the solr wiki [6]. + +If you feel some service option is missing, or have other ideas for improving +this implementation, please file a feature request in the project's issue queue, +at [7]. + +[4] https://drupal.org/node/2004596 +[5] https://drupal.org/sandbox/daspeter/1783280 +[6] http://wiki.apache.org/solr/FieldCollapsing +[7] https://drupal.org/project/issues/search_api_solr + +Specifics +--------- + +Please consider that, since Solr handles tokenizing, stemming and other +preprocessing tasks, activating any preprocessors in a search index' settings is +usually not needed or even cumbersome. If you are adding an index to a Solr +server you should therefore then disable all processors which handle such +classic preprocessing tasks. Enabling the HTML filter can be useful, though, as +the default config files included in this module don't handle stripping out HTML +tags. + +Also, due to the way Solr works, using a single field for fulltext searching +will result in the smallest index size and best search performance, as well as +possibly having other advantages, too. Therefore, if you don't need to search +different sets of fields in different searches on an index, it is adviced that +you collect all fields that should be searchable into a single field using the +“Aggregated fields” data alteration. + +Clean field identifiers: + If your Solr server was created in a module version prior to 1.2, you will get + the option to switch the server to "Clean field identifiers" (which is default + for all new servers). This will change the Solr field names used for all + fields whose Search API identifiers contain a colon (i.e., all nested fields) + to support some advanced functionality, like sorting by distance, for which + Solr is buggy when using field names with colons. + The only downside of this change is that the data in Solr for these fields + will become invalid, so all indexes on the server which contain such fields + will be scheduled for re-indexing. (If you don't want to search on incomplete + data until the re-indexing is finished, you can additionally manually clear + the indexes, on their Status tabs, to prevent this.) + +Hidden variables +---------------- + +- search_api_solr_autocomplete_max_occurrences (default: 0.9) + By default, keywords that occur in more than 90% of results are ignored for + autocomplete suggestions. This setting lets you modify that behaviour by + providing your own ratio. Use 1 or greater to use all suggestions. +- search_api_solr_index_prefix (default: '') + By default, the index ID in the Solr server is the same as the index's machine + name in Drupal. This setting will let you specify a prefix for the index IDs + on this Drupal installation. Only use alphanumeric characters and underscores. + Since changing the prefix makes the currently indexed data inaccessible, you + should change this vairable only when no indexes are currently on any Solr + servers. +- search_api_solr_index_prefix_INDEX_ID (default: '') + Same as above, but a per-index prefix. Use the index's machine name as + INDEX_ID in the variable name. Per-index prefixing is done before the global + prefix is added, so the global prefix will come first in the final name: + (GLOBAL_PREFIX)(INDEX_PREFIX)(INDEX_ID) + The same rules as above apply for setting the prefix. +- search_api_solr_http_get_max_length (default: 4000) + The maximum number of bytes that can be handled as an HTTP GET query when + HTTP method is AUTO. Typically Solr can handle up to 65355 bytes, but Tomcat + and Jetty will error at slightly less than 4096 bytes. + +Customizing your Solr server +---------------------------- + +The schema.xml and solrconfig.xml files contain extensive comments on how to +add additional features or modify behaviour, e.g., for adding a language- +specific stemmer or a stopword list. +If you are interested in further customizing your Solr server to your needs, +see the Solr wiki at [8] for documentation. When editing the schema.xml and +solrconfig.xml files, please only edit the copies in the Solr configuration +directory, not directly the ones provided with this module. + +[8] http://wiki.apache.org/solr/ + +You'll have to restart your Solr server after making such changes, for them to +take effect. + +Developers +---------- + +The SearchApiSolrService class has a few custom extensions, documented with its +code. Methods of note are deleteItems(), which treats the first argument +differently in certain cases, and the methods at the end of service.inc. diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/document.inc b/sites/all/modules/contrib/search/search_api_solr/includes/document.inc new file mode 100644 index 00000000..010744f5 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/includes/document.inc @@ -0,0 +1,435 @@ + + */ + +/** + * Additional code Copyright (c) 2011 by Peter Wolanin, and + * additional contributors. + * + * 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. + */ + +/** + * Holds Key / Value pairs that represent a Solr Document along with any + * associated boost values. Field values can be accessed by direct dereferencing + * such as: + * + * @code + * $document->title = 'Something'; + * echo $document->title; + * @endcode + * + * Additionally, the field values can be iterated with foreach: + * + * @code + * foreach ($document as $fieldName => $fieldValue) { + * // ... + * } + * @endcode + */ +class SearchApiSolrDocument implements IteratorAggregate { + + /** + * Document boost value. + * + * @var float|false + */ + protected $documentBoost = FALSE; + + /** + * Document field values, indexed by name. + * + * @var array + */ + protected $fields = array(); + + /** + * Document field boost values, indexed by name. + * + * @var array + */ + protected $fieldBoosts = array(); + + /** + * Clears all boosts and fields from this document. + */ + public function clear() { + $this->documentBoost = FALSE; + + $this->fields = array(); + $this->fieldBoosts = array(); + } + + /** + * Gets the current document boost. + * + * @return float|false + * The current document boost, or FALSE if none is set. + */ + public function getBoost() { + return $this->documentBoost; + } + + /** + * Sets the document boost factor. + * + * @param float|false $boost + * FALSE for default boost, or a positive number for setting a document + * boost. + */ + public function setBoost($boost) { + $boost = (float) $boost; + + if ($boost > 0.0) { + $this->documentBoost = $boost; + } + else { + $this->documentBoost = FALSE; + } + } + + /** + * Adds a value to a multi-valued field + * + * NOTE: the solr XML format allows you to specify boosts PER value even + * though the underlying Lucene implementation only allows a boost per field. + * To remedy this, the final field boost value will be the product of all + * specified boosts on field values - this is similar to SolrJ's + * functionality. + * + * @code + * $doc = new ApacheSolrDocument(); + * $doc->addField('foo', 'bar', 2.0); + * $doc->addField('foo', 'baz', 3.0); + * // Resultant field boost will be 6! + * echo $doc->getFieldBoost('foo'); + * @endcode + * + * @param string $key + * The name of the field. + * @param $value + * The value to add for the field. + * @param float|false $boost + * FALSE for default boost, or a positive number for setting a field boost. + */ + public function addField($key, $value, $boost = FALSE) { + if (!isset($this->fields[$key])) { + // create holding array if this is the first value + $this->fields[$key] = array(); + } + else if (!is_array($this->fields[$key])) { + // move existing value into array if it is not already an array + $this->fields[$key] = array($this->fields[$key]); + } + + if ($this->getFieldBoost($key) === FALSE) { + // boost not already set, set it now + $this->setFieldBoost($key, $boost); + } + else if ((float) $boost > 0.0) { + // multiply passed boost with current field boost - similar to SolrJ implementation + $this->fieldBoosts[$key] *= (float) $boost; + } + + // add value to array + $this->fields[$key][] = $value; + } + + /** + * Gets information about a field stored in Solr. + * + * @param string $key + * The name of the field. + * + * @return array|false + * An associative array of info if the field exists, FALSE otherwise. + */ + public function getField($key) { + if (isset($this->fields[$key])) { + return array( + 'name' => $key, + 'value' => $this->fields[$key], + 'boost' => $this->getFieldBoost($key) + ); + } + + return FALSE; + } + + /** + * Sets a field value. + * + * Multi-valued fields should be set as arrays or via the addField() + * function which will automatically make sure the field is an array. + * + * @param string $key + * The name of the field. + * @param string|array $value + * The value to set for the field. + * @param float|false $boost + * FALSE for default boost, or a positive number for setting a field boost. + */ + public function setField($key, $value, $boost = FALSE) { + $this->fields[$key] = $value; + $this->setFieldBoost($key, $boost); + } + + /** + * Gets the currently set field boost for a document field. + * + * @param string $key + * The name of the field. + * + * @return float|false + * The currently set field boost, or FALSE if none was set. + */ + public function getFieldBoost($key) { + return isset($this->fieldBoosts[$key]) ? $this->fieldBoosts[$key] : FALSE; + } + + /** + * Sets the field boost for a document field. + * + * @param string $key + * The name of the field. + * @param float|false $boost + * FALSE for default boost, or a positive number for setting a field boost. + */ + public function setFieldBoost($key, $boost) { + $boost = (float) $boost; + + if ($boost > 0.0) { + $this->fieldBoosts[$key] = $boost; + } + else { + $this->fieldBoosts[$key] = FALSE; + } + } + + /** + * Returns all current field boosts, indexed by field name. + * + * @return array + * An associative array in the format $field_name => $field_boost. + */ + public function getFieldBoosts() { + return $this->fieldBoosts; + } + + /** + * Gets the names of all fields in this document. + * + * @return array + * The names of all fields in this document. + */ + public function getFieldNames() { + return array_keys($this->fields); + } + + /** + * Gets the values of all fields in this document. + * + * @return array + * The values of all fields in this document. + */ + public function getFieldValues() { + return array_values($this->fields); + } + + /** + * Implements IteratorAggregate::getIterator(). + * + * Implementing the IteratorAggregate interface allows the following usage: + * @code + * foreach ($document as $key => $value) { + * // ... + * } + * @endcode + * + * @return Traversable + * An iterator over this document's fields. + */ + public function getIterator() { + $arrayObject = new ArrayObject($this->fields); + + return $arrayObject->getIterator(); + } + + /** + * Magic getter for field values. + * + * @param string $key + * The name of the field. + * + * @return string|array|null + * The value that was set for the field. + */ + public function __get($key) { + return $this->fields[$key]; + } + + /** + * Magic setter for field values. + * + * Multi-valued fields should be set as arrays or via the addField() function + * which will automatically make sure the field is an array. + * + * @param string $key + * The name of the field. + * @param string|array $value + * The value to set for the field. + */ + public function __set($key, $value) { + $this->setField($key, $value); + } + + /** + * Magic isset for fields values. + * + * Do not call directly. Allows the following usage: + * @code + * isset($document->some_field); + * @endcode + * + * @param string $key + * The name of the field. + * + * @return bool + * Whether the given key is set in this document. + */ + public function __isset($key) { + return isset($this->fields[$key]); + } + + /** + * Magic unset for field values. + * + * Do not call directly. Allows the following usage: + * @code + * unset($document->some_field); + * @endcode + * + * @param string $key + * The name of the field. + */ + public function __unset($key) { + unset($this->fields[$key]); + unset($this->fieldBoosts[$key]); + } + + /** + * Create an XML fragment from this document. + * + * This string can then be used inside a Solr add call. + * + * @return string + * An XML formatted string for this document. + */ + public function toXml() { + $xml = 'documentBoost !== FALSE) { + $xml .= ' boost="' . $this->documentBoost . '"'; + } + + $xml .= '>'; + + foreach ($this->fields as $key => $value) { + $fieldBoost = $this->getFieldBoost($key); + $key = htmlspecialchars($key, ENT_COMPAT, 'UTF-8'); + + if (is_array($value)) { + foreach ($value as $multivalue) { + $xml .= ''; + } + } + else { + $xml .= ''; + } + } + + $xml .= ''; + + // Remove any control characters to avoid Solr XML parser exception. + return self::stripCtrlChars($xml); + } + + /** + * Sanitizes XML for sending to Solr. + * + * Replaces control (non-printable) characters that are invalid to Solr's XML + * parser with a space. + * + * @param string $string + * The string to sanitize. + * + * @return string + * A string safe for including in a Solr request. + */ + public static function stripCtrlChars($string) { + // See: http://w3.org/International/questions/qa-forms-utf-8.html + // Printable utf-8 does not include any of these chars below x7F + return preg_replace('@[\x00-\x08\x0B\x0C\x0E-\x1F]@', ' ', $string); + } +} \ No newline at end of file diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/service.inc b/sites/all/modules/contrib/search/search_api_solr/includes/service.inc new file mode 100644 index 00000000..964d262f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/includes/service.inc @@ -0,0 +1,2166 @@ +options) { + // Editing this server + $form['server_description'] = array( + '#type' => 'item', + '#title' => t('Solr server URI'), + '#description' => $this->getServerLink(), + ); + } + + $options = $this->options + array( + 'scheme' => 'http', + 'host' => 'localhost', + 'port' => '8983', + 'path' => '/solr', + 'http_user' => '', + 'http_pass' => '', + 'excerpt' => FALSE, + 'retrieve_data' => FALSE, + 'highlight_data' => FALSE, + 'http_method' => 'AUTO', + // Default to TRUE for new servers, but to FALSE for existing ones. + 'clean_ids' => $this->options ? FALSE : TRUE, + 'autocorrect_spell' => TRUE, + 'autocorrect_suggest_words' => TRUE, + ); + + if (!$options['clean_ids']) { + if (module_exists('advanced_help')) { + $variables['@url']= url('help/search_api_solr/README.txt'); + } + else { + $variables['@url']= url(drupal_get_path('module', 'search_api_solr') . '/README.txt'); + } + $description = t('Change Solr field names to be more compatible with advanced features. Doing this leads to re-indexing of all indexes on this server. See README.txt for details.', $variables); + $form['clean_ids_form'] = array( + '#type' => 'fieldset', + '#title' => t('Clean field identifiers'), + '#description' => $description, + '#collapsible' => TRUE, + ); + $form['clean_ids_form']['submit'] = array( + '#type' => 'submit', + '#value' => t('Switch to clean field identifiers'), + '#submit' => array('_search_api_solr_switch_to_clean_ids'), + ); + } + $form['clean_ids'] = array( + '#type' => 'value', + '#value' => $options['clean_ids'], + ); + + $form['scheme'] = array( + '#type' => 'select', + '#title' => t('HTTP protocol'), + '#description' => t('The HTTP protocol to use for sending queries.'), + '#default_value' => $options['scheme'], + '#options' => array( + 'http' => 'http', + 'https' => 'https', + ), + ); + + $form['host'] = array( + '#type' => 'textfield', + '#title' => t('Solr host'), + '#description' => t('The host name or IP of your Solr server, e.g. localhost or www.example.com.'), + '#default_value' => $options['host'], + '#required' => TRUE, + ); + $form['port'] = array( + '#type' => 'textfield', + '#title' => t('Solr port'), + '#description' => t('The Jetty example server is at port 8983, while Tomcat uses 8080 by default.'), + '#default_value' => $options['port'], + '#required' => TRUE, + ); + $form['path'] = array( + '#type' => 'textfield', + '#title' => t('Solr path'), + '#description' => t('The path that identifies the Solr instance to use on the server.'), + '#default_value' => $options['path'], + ); + + $form['http'] = array( + '#type' => 'fieldset', + '#title' => t('Basic HTTP authentication'), + '#description' => t('If your Solr server is protected by basic HTTP authentication, enter the login data here.'), + '#collapsible' => TRUE, + '#collapsed' => empty($options['http_user']), + ); + $form['http']['http_user'] = array( + '#type' => 'textfield', + '#title' => t('Username'), + '#default_value' => $options['http_user'], + ); + $form['http']['http_pass'] = array( + '#type' => 'password', + '#title' => t('Password'), + '#description' => t('If this field is left blank and the HTTP username is filled out, the current password will not be changed.'), + ); + + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['advanced']['excerpt'] = array( + '#type' => 'checkbox', + '#title' => t('Return an excerpt for all results'), + '#description' => t("If search keywords are given, use Solr's capabilities to create a highlighted search excerpt for each result. " . + 'Whether the excerpts will actually be displayed depends on the settings of the search, though.'), + '#default_value' => $options['excerpt'], + ); + $form['advanced']['retrieve_data'] = array( + '#type' => 'checkbox', + '#title' => t('Retrieve result data from Solr'), + '#description' => t('When checked, result data will be retrieved directly from the Solr server. ' . + 'This might make item loads unnecessary. Only indexed fields can be retrieved. ' . + 'Note also that the returned field data might not always be correct, due to preprocessing and caching issues.'), + '#default_value' => $options['retrieve_data'], + ); + $form['advanced']['highlight_data'] = array( + '#type' => 'checkbox', + '#title' => t('Highlight retrieved data'), + '#description' => t('When retrieving result data from the Solr server, try to highlight the search terms in the returned fulltext fields.'), + '#default_value' => $options['highlight_data'], + ); + // Highlighting retrieved data only makes sense when we retrieve data. + // (Actually, internally it doesn't really matter. However, from a user's + // perspective, having to check both probably makes sense.) + $form['advanced']['highlight_data']['#states']['invisible'] + [':input[name="options[form][advanced][retrieve_data]"]']['checked'] = FALSE; + + $form['advanced']['http_method'] = array( + '#type' => 'select', + '#title' => t('HTTP method'), + '#description' => t('The HTTP method to use for sending queries. GET will often fail with larger queries, while POST should not be cached. AUTO will use GET when possible, and POST for queries that are too large.'), + '#default_value' => $options['http_method'], + '#options' => array( + 'AUTO' => t('AUTO'), + 'POST' => 'POST', + 'GET' => 'GET', + ), + ); + + if (module_exists('search_api_autocomplete')) { + $form['advanced']['autocomplete'] = array( + '#type' => 'fieldset', + '#title' => t('Autocomplete'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['advanced']['autocomplete']['autocorrect_spell'] = array( + '#type' => 'checkbox', + '#title' => t('Use spellcheck for autocomplete suggestions'), + '#description' => t('If activated, spellcheck suggestions ("Did you mean") will be included in the autocomplete suggestions. Since the used dictionary contains words from all indexes, this might lead to leaking of sensitive data, depending on your setup.'), + '#default_value' => $options['autocorrect_spell'], + ); + $form['advanced']['autocomplete']['autocorrect_suggest_words'] = array( + '#type' => 'checkbox', + '#title' => t('Suggest additional words'), + '#description' => t('If activated and the user enters a complete word, Solr will suggest additional words the user wants to search, which are often found (not searched!) together. This has been known to lead to strange results in some configurations – if you see inappropriate additional-word suggestions, you might want to deactivate this option.'), + '#default_value' => $options['autocorrect_suggest_words'], + ); + } + + return $form; + } + + /** + * Overrides SearchApiAbstractService::configurationFormValidate(). + */ + public function configurationFormValidate(array $form, array &$values, array &$form_state) { + if (isset($values['port']) && (!is_numeric($values['port']) || $values['port'] < 0 || $values['port'] > 65535)) { + form_error($form['port'], t('The port has to be an integer between 0 and 65535.')); + } + } + + /** + * Overrides SearchApiAbstractService::configurationFormSubmit(). + */ + public function configurationFormSubmit(array $form, array &$values, array &$form_state) { + // Since the form is nested into another, we can't simply use #parents for + // doing this array restructuring magic. (At least not without creating an + // unnecessary dependency on internal implementation.) + $values += $values['http']; + $values += $values['advanced']; + $values += !empty($values['autocomplete']) ? $values['autocomplete'] : array(); + unset($values['http'], $values['advanced'], $values['autocomplete']); + + // Highlighting retrieved data only makes sense when we retrieve data. + $values['highlight_data'] &= $values['retrieve_data']; + + // For password fields, there is no default value, they're empty by default. + // Therefore we ignore empty submissions if the user didn't change either. + if ($values['http_pass'] === '' + && isset($this->options['http_user']) + && $values['http_user'] === $this->options['http_user']) { + $values['http_pass'] = $this->options['http_pass']; + } + + parent::configurationFormSubmit($form, $values, $form_state); + } + + /** + * {@inheritdoc} + */ + public function supportsFeature($feature) { + // First, check the features we always support. + $supported = drupal_map_assoc(array( + 'search_api_autocomplete', + 'search_api_facets', + 'search_api_facets_operator_or', + 'search_api_grouping', + 'search_api_mlt', + 'search_api_multi', + 'search_api_service_extra', + 'search_api_spellcheck', + 'search_api_data_type_location', + 'search_api_data_type_geohash', + )); + if (isset($supported[$feature])) { + return TRUE; + } + + // If it is a custom data type, maybe we support it automatically via + // search_api_solr_hook_search_api_data_type_info(). + if (substr($feature, 0, 21) != 'search_api_data_type_') { + return FALSE; + } + $type = substr($feature, 21); + $type = search_api_get_data_type_info($type); + // We only support it if the "prefix" key is set. + return $type && !empty($type['prefix']); + } + + /** + * Overrides SearchApiAbstractService::viewSettings(). + * + * Returns an empty string since information is instead added via + * getExtraInformation(). + */ + public function viewSettings() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function getExtraInformation() { + $info = array(); + + $info[] = array( + 'label' => t('Solr server URI'), + 'info' => $this->getServerLink(), + ); + + if ($this->options['http_user']) { + $vars = array( + '@user' => $this->options['http_user'], + '@pass' => str_repeat('*', strlen($this->options['http_pass'])), + ); + $http = t('Username: @user; Password: @pass', $vars); + $info[] = array( + 'label' => t('Basic HTTP authentication'), + 'info' => $http, + ); + } + + if ($this->server->enabled) { + // If the server is enabled, check whether Solr can be reached. + $ping = $this->ping(); + if ($ping) { + $msg = t('The Solr server could be reached (latency: @millisecs ms).', array('@millisecs' => $ping * 1000)); + } + else { + $msg = t('The Solr server could not be reached. Further data is therefore unavailable.'); + } + $info[] = array( + 'label' => t('Connection'), + 'info' => $msg, + 'status' => $ping ? 'ok' : 'error', + ); + + if ($ping) { + try { + // If Solr can be reached, provide more information. This isn't done + // often (only when an admin views the server details), so we clear the + // cache to get the current data. + $this->connect(); + $this->solr->clearCache(); + $data = $this->solr->getLuke(); + if (isset($data->index->numDocs)) { + // Collect the stats + $stats_summary = $this->solr->getStatsSummary(); + + $pending_msg = $stats_summary['@pending_docs'] ? t('(@pending_docs sent but not yet processed)', $stats_summary) : ''; + $index_msg = $stats_summary['@index_size'] ? t('(@index_size on disk)', $stats_summary) : ''; + $indexed_message = t('@num items !pending !index_msg', array( + '@num' => $data->index->numDocs, + '!pending' => $pending_msg, + '!index_msg' => $index_msg, + )); + $info[] = array( + 'label' => t('Indexed'), + 'info' => $indexed_message, + ); + + if (!empty($stats_summary['@deletes_total'])) { + $info[] = array( + 'label' => t('Pending Deletions'), + 'info' => $stats_summary['@deletes_total'], + ); + } + + $info[] = array( + 'label' => t('Delay'), + 'info' => t('@autocommit_time before updates are processed.', $stats_summary), + ); + + $status = 'ok'; + if (substr($stats_summary['@schema_version'], 0, 10) == 'search-api') { + drupal_set_message(t('Your schema.xml version is too old. Please replace all configuration files with the ones packaged with this module and re-index you data.'), 'error'); + $status = 'error'; + } + elseif (substr($stats_summary['@schema_version'], 0, 9) != 'drupal-4.') { + $variables['@url'] = url(drupal_get_path('module', 'search_api_solr') . '/INSTALL.txt'); + $message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in the INSTALL.txt file for setting up Solr.', $variables); + drupal_set_message($message, 'error'); + $status = 'error'; + } + $info[] = array( + 'label' => t('Schema'), + 'info' => $stats_summary['@schema_version'], + 'status' => $status, + ); + + if (!empty($stats_summary['@core_name'])) { + $info[] = array( + 'label' => t('Solr Core Name'), + 'info' => $stats_summary['@core_name'], + ); + } + } + } + catch (SearchApiException $e) { + $info[] = array( + 'label' => t('Additional information'), + 'info' => t('An error occurred while trying to retrieve additional information from the Solr server: @msg.', array('@msg' => $e->getMessage())), + 'status' => 'error', + ); + } + } + } + + return $info; + } + + /** + * Returns a link to the Solr server, if the necessary options are set. + */ + public function getServerLink() { + if (!$this->options) { + return ''; + } + $host = $this->options['host']; + if ($host == 'localhost' && !empty($_SERVER['SERVER_NAME'])) { + $host = $_SERVER['SERVER_NAME']; + } + $url = $this->options['scheme'] . '://' . $host . ':' . $this->options['port'] . $this->options['path']; + return l($url, $url); + } + + /** + * Create a connection to the Solr server as configured in $this->options. + */ + protected function connect() { + if (!$this->solr) { + if (!class_exists($this->connection_class)) { + throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $this->connection_class))); + } + $options = $this->options + array('server' => $this->server->machine_name); + $this->solr = new $this->connection_class($options); + if (!($this->solr instanceof SearchApiSolrConnectionInterface)) { + $this->solr = NULL; + throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $this->connection_class))); + } + } + } + + /** + * Overrides SearchApiAbstractService::addIndex(). + */ + public function addIndex(SearchApiIndex $index) { + if (module_exists('search_api_multi') && module_exists('search_api_views')) { + views_invalidate_cache(); + } + } + + /** + * Overrides SearchApiAbstractService::fieldsUpdated(). + */ + public function fieldsUpdated(SearchApiIndex $index) { + if (module_exists('search_api_multi') && module_exists('search_api_views')) { + views_invalidate_cache(); + } + return TRUE; + } + + /** + * Overrides SearchApiAbstractService::removeIndex(). + */ + public function removeIndex($index) { + if (module_exists('search_api_multi') && module_exists('search_api_views')) { + views_invalidate_cache(); + } + $id = is_object($index) ? $index->machine_name : $index; + // Only delete the index's data if the index isn't read-only. + if (!is_object($index) || empty($index->read_only)) { + $this->connect(); + try { + $this->solr->deleteByQuery("index_id:" . $this->getIndexId($id)); + } + catch (Exception $e) { + throw new SearchApiException($e->getMessage()); + } + } + } + + /** + * Implements SearchApiServiceInterface::indexItems(). + */ + public function indexItems(SearchApiIndex $index, array $items) { + $documents = array(); + $ret = array(); + $index_id = $this->getIndexId($index->machine_name); + $fields = $this->getFieldNames($index); + + foreach ($items as $id => $item) { + try { + $doc = new SearchApiSolrDocument(); + $doc->setField('id', $this->createId($index_id, $id)); + $doc->setField('index_id', $index_id); + $doc->setField('item_id', $id); + + foreach ($item as $key => $field) { + if (!isset($fields[$key])) { + throw new SearchApiException(t('Unknown field @field.', array('@field' => $key))); + } + $this->addIndexField($doc, $fields[$key], $field['value'], $field['type']); + } + + $documents[] = $doc; + $ret[] = $id; + } + catch (Exception $e) { + watchdog_exception('search_api_solr', $e, "%type while indexing @type with ID @id: !message in %function (line %line of %file).", array('@type' => $index->item_type, '@id' => $id), WATCHDOG_WARNING); + } + } + + // Let other modules alter documents before sending them to solr. + drupal_alter('search_api_solr_documents', $documents, $index, $items); + $this->alterSolrDocuments($documents, $index, $items); + + if (!$documents) { + return array(); + } + try { + $this->connect(); + $this->solr->addDocuments($documents); + if (!empty($index->options['index_directly'])) { + $this->scheduleCommit(); + } + return $ret; + } + catch (SearchApiException $e) { + watchdog_exception('search_api_solr', $e, "%type while indexing: !message in %function (line %line of %file)."); + } + return array(); + } + + /** + * Creates an ID used as the unique identifier at the Solr server. + * + * This has to consist of both index and item ID. + */ + protected function createId($index_id, $item_id) { + return "$index_id-$item_id"; + } + + /** + * Create a list of all indexed field names mapped to their Solr field names. + * + * The special fields "search_api_id" and "search_api_relevance" are also + * included. Any Solr fields that exist on search results are mapped back to + * to their local field names in the final result set. + * + * @see SearchApiSolrService::search() + */ + public function getFieldNames(SearchApiIndex $index, $reset = FALSE) { + if (!isset($this->fieldNames[$index->machine_name]) || $reset) { + // This array maps "local property name" => "solr doc property name". + $ret = array( + 'search_api_id' => 'item_id', + 'search_api_relevance' => 'score', + ); + + // Add the names of any fields configured on the index. + $fields = (isset($index->options['fields']) ? $index->options['fields'] : array()); + foreach ($fields as $key => $field) { + // Generate a field name; this corresponds with naming conventions in + // our schema.xml + $type = $field['type']; + + // Use the real type of the field if the server supports this type. + if (isset($field['real_type'])) { + $custom_type = search_api_extract_inner_type($field['real_type']); + if ($this->supportsFeature('search_api_data_type_' . $custom_type)) { + $type = $field['real_type']; + } + } + + $inner_type = search_api_extract_inner_type($type); + $type_info = search_api_solr_get_data_type_info($inner_type); + $pref = isset($type_info['prefix']) ? $type_info['prefix']: ''; + if (empty($type_info['always multiValued'])) { + $pref .= ($type == $inner_type) ? 's' : 'm'; + } + if (!empty($this->options['clean_ids'])) { + $name = $pref . '_' . str_replace(':', '$', $key); + } + else { + $name = $pref . '_' . $key; + } + + $ret[$key] = $name; + } + + // Let modules adjust the field mappings. + drupal_alter('search_api_solr_field_mapping', $index, $ret); + + $this->fieldNames[$index->machine_name] = $ret; + } + + return $this->fieldNames[$index->machine_name]; + } + + /** + * Helper method for indexing. + * + * Adds $value with field name $key to the document $doc. The format of $value + * is the same as specified in SearchApiServiceInterface::indexItems(). + */ + protected function addIndexField(SearchApiSolrDocument $doc, $key, $value, $type, $multi_valued = FALSE) { + // Don't index empty values (i.e., when field is missing). + if (!isset($value)) { + return; + } + if (search_api_is_list_type($type)) { + $type = substr($type, 5, -1); + foreach ($value as $v) { + $this->addIndexField($doc, $key, $v, $type, TRUE); + } + return; + } + switch ($type) { + case 'tokens': + foreach ($value as $v) { + $doc->addField($key, $v['value']); + } + return; + case 'boolean': + $value = $value ? 'true' : 'false'; + break; + case 'date': + $value = is_numeric($value) ? (int) $value : strtotime($value); + if ($value === FALSE) { + return; + } + $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC'); + break; + case 'integer': + $value = (int) $value; + break; + case 'decimal': + $value = (float) $value; + break; + } + if ($multi_valued) { + $doc->addField($key, $value); + } + else { + $doc->setField($key, $value); + } + } + + /** + * Applies custom modifications to indexed Solr documents. + * + * This method allows subclasses to easily apply custom changes before the + * documents are sent to Solr. The method is empty by default. + * + * @param array $documents + * An array of SearchApiSolrDocument objects ready to be indexed, generated + * from $items array. + * @param SearchApiIndex $index + * The search index for which items are being indexed. + * @param array $items + * An array of items being indexed. + * + * @see hook_search_api_solr_documents_alter() + */ + protected function alterSolrDocuments(array &$documents, SearchApiIndex $index, array $items) { + } + + /** + * Implements SearchApiServiceInterface::deleteItems(). + * + * This method has a custom, Solr-specific extension: + * + * If $ids is a string other than "all", it is treated as a Solr query. All + * items matching that Solr query are then deleted. If $index is additionally + * specified, then only those items also lying on that index will be deleted. + * + * It is up to the caller to ensure $ids is a valid query when the method is + * called in this fashion. + */ + public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) { + $this->connect(); + if ($index) { + $index_id = $this->getIndexId($index->machine_name); + if (is_array($ids)) { + $solr_ids = array(); + foreach ($ids as $id) { + $solr_ids[] = $this->createId($index_id, $id); + } + $this->solr->deleteByMultipleIds($solr_ids); + } + elseif ($ids == 'all') { + $this->solr->deleteByQuery("index_id:" . $index_id); + } + else { + $this->solr->deleteByQuery("index_id:" . $index_id . ' (' . $ids . ')'); + } + } + else { + $q = $ids == 'all' ? '*:*' : $ids; + $this->solr->deleteByQuery($q); + } + $this->scheduleCommit(); + } + + /** + * Implements SearchApiServiceInterface::search(). + */ + public function search(SearchApiQueryInterface $query) { + $time_method_called = microtime(TRUE); + // Reset request handler. + $this->request_handler = NULL; + // Get field information. + $index = $query->getIndex(); + $index_id = $this->getIndexId($index->machine_name); + $fields = $this->getFieldNames($index); + // Get Solr connection. + $this->connect(); + $version = $this->solr->getSolrVersion(); + + // Extract keys. + $keys = $query->getKeys(); + if (is_array($keys)) { + $keys = $this->flattenKeys($keys); + } + + // Set searched fields. + $options = $query->getOptions(); + $search_fields = $query->getFields(); + // Get the index fields to be able to retrieve boosts. + $index_fields = $index->getFields(); + $qf = array(); + foreach ($search_fields as $f) { + $boost = ''; + $boost = isset($index_fields[$f]['boost']) ? '^' . $index_fields[$f]['boost'] : ''; + $qf[] = $fields[$f] . $boost; + } + + // Extract filters. + $filter = $query->getFilter(); + $fq = $this->createFilterQueries($filter, $fields, $index->options['fields']); + $fq[] = 'index_id:' . $index_id; + + // Extract sort. + $sort = array(); + foreach ($query->getSort() as $field => $order) { + $f = $fields[$field]; + if (substr($f, 0, 3) == 'ss_') { + $f = 'sort_' . substr($f, 3); + } + $order = strtolower($order); + $sort[$field] = "$f $order"; + } + + // Get facet fields. + $facets = $query->getOption('search_api_facets', array()); + $facet_params = $this->getFacetParams($facets, $fields, $fq); + + // Handle highlighting. + $highlight_params = $this->getHighlightParams($query); + + // Handle More Like This query. + $mlt = $query->getOption('search_api_mlt'); + if ($mlt) { + $mlt_params['qt'] = 'mlt'; + // The fields to look for similarities in. + $mlt_fl = array(); + foreach($mlt['fields'] as $f) { + // Solr 4 has a bug which results in numeric fields not being supported + // in MLT queries. + // Date fields don't seem to be supported at all. + if ($fields[$f][0] === 'd' || ($version == 4 && in_array($fields[$f][0], array('i', 'f')))) { + continue; + } + $mlt_fl[] = $fields[$f]; + // For non-text fields, set minimum word length to 0. + if (isset($index->options['fields'][$f]['type']) && !search_api_is_text_type($index->options['fields'][$f]['type'])) { + $mlt_params['f.' . $fields[$f] . '.mlt.minwl'] = 0; + } + } + $mlt_params['mlt.fl'] = implode(',', $mlt_fl); + $id = $this->createId($index_id, $mlt['id']); + $id = call_user_func(array($this->connection_class, 'phrase'), $id); + $keys = 'id:' . $id; + } + + // Handle spatial filters. + if ($spatials = $query->getOption('search_api_location')) { + foreach ($spatials as $i => $spatial) { + if (empty($spatial['field']) || empty($spatial['lat']) || empty($spatial['lon'])) { + continue; + } + + unset($radius); + $field = $fields[$spatial['field']]; + $escaped_field = SearchApiSolrConnection::escapeFieldName($field); + $point = ((float) $spatial['lat']) . ',' . ((float) $spatial['lon']); + + // Prepare the filter settings. + if (isset($spatial['radius'])) { + $radius = (float) $spatial['radius']; + } + $spatial_method = 'geofilt'; + if (isset($spatial['method']) && in_array($spatial['method'], array('geofilt', 'bbox'))) { + $spatial_method = $spatial['method']; + } + + // Change the fq facet ranges to the correct fq. + foreach ($fq as $key => $value) { + // If the fq consists only of a filter on this field, replace it with + // a range. + $preg_field = preg_quote($escaped_field, '/'); + if (preg_match('/^' . $preg_field . ':\["?(\*|\d+(?:\.\d+)?)"? TO "?(\*|\d+(?:\.\d+)?)"?\]$/', $value, $m)) { + unset($fq[$key]); + if ($m[1] && is_numeric($m[1])) { + $min_radius = isset($min_radius) ? max($min_radius, $m[1]) : $m[1]; + } + if (is_numeric($m[2])) { + // Make the radius tighter accordingly. + $radius = isset($radius) ? min($radius, $m[2]) : $m[2]; + } + } + } + + // If either a radius was given in the option, or a filter was + // encountered, set a filter for the lowest value. If a lower boundary + // was set (too), we can only set a filter for that if the field name + // doesn't contains any colons. + if (isset($min_radius) && strpos($field, ':') === FALSE) { + $upper = isset($radius) ? " u=$radius" : ''; + $fq[] = "{!frange l=$min_radius$upper}geodist($field,$point)"; + } + elseif (isset($radius)) { + $fq[] = "{!$spatial_method pt=$point sfield=$field d=$radius}"; + } + + // Change sort on the field, if set (and not already changed). + if (isset($sort[$spatial['field']]) && substr($sort[$spatial['field']], 0, strlen($field)) === $field) { + if (strpos($field, ':') === FALSE) { + $sort[$spatial['field']] = str_replace($field, "geodist($field,$point)", $sort[$spatial['field']]); + } + else { + $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit'); + watchdog('search_api_solr', 'Location sort on field @field had to be ignored because unclean field identifiers are used.', array('@field' => $spatial['field']), WATCHDOG_WARNING, $link); + } + } + + // Change the facet parameters for spatial fields to return distance + // facets. + if (!empty($facets)) { + if (!empty($facet_params['facet.field'])) { + $facet_params['facet.field'] = array_diff($facet_params['facet.field'], array($field)); + } + foreach ($facets as $delta => $facet) { + if ($facet['field'] != $spatial['field']) { + continue; + } + $steps = $facet['limit'] > 0 ? $facet['limit'] : 5; + $step = (isset($radius) ? $radius : 100) / $steps; + for ($k = $steps - 1; $k > 0; --$k) { + $distance = $step * $k; + $key = "spatial-$delta-$distance"; + $facet_params['facet.query'][] = "{!$spatial_method pt=$point sfield=$field d=$distance key=$key}"; + } + foreach (array('limit', 'mincount', 'missing') as $setting) { + unset($facet_params["f.$field.facet.$setting"]); + } + } + } + } + } + // Normal sorting on location fields isn't possible. + foreach ($sort as $field => $sort_param) { + if (substr($sort_param, 0, 3) === 'loc') { + unset($sort[$field]); + } + } + + // Handle field collapsing / grouping. + $grouping = $query->getOption('search_api_grouping'); + if (!empty($grouping['use_grouping'])) { + $group_params['group'] = 'true'; + // We always want the number of groups returned so that we get pagers done + // right. + $group_params['group.ngroups'] = 'true'; + if (!empty($grouping['truncate'])) { + $group_params['group.truncate'] = 'true'; + } + if (!empty($grouping['group_facet'])) { + $group_params['group.facet'] = 'true'; + } + foreach ($grouping['fields'] as $collapse_field) { + $type = $index_fields[$collapse_field]['type']; + // Only single-valued fields are supported. + if ($version < 4) { + // For Solr 3.x, only string and boolean fields are supported. + if (search_api_is_list_type($type) || !search_api_is_text_type($type, array('string', 'boolean', 'uri'))) { + $warnings[] = t('Grouping is not supported for field @field. ' . + 'Only single-valued fields of type "String", "Boolean" or "URI" are supported.', + array('@field' => $index_fields[$collapse_field]['name'])); + continue; + } + } + else { + if (search_api_is_list_type($type) || search_api_is_text_type($type)) { + $warnings[] = t('Grouping is not supported for field @field. ' . + 'Only single-valued fields not indexed as "Fulltext" are supported.', + array('@field' => $index_fields[$collapse_field]['name'])); + continue; + } + } + $group_params['group.field'][] = $fields[$collapse_field]; + } + if (empty($group_params['group.field'])) { + unset($group_params); + } + else { + if (!empty($grouping['group_sort'])) { + foreach ($grouping['group_sort'] as $group_sort_field => $order) { + if (isset($fields[$group_sort_field])) { + $f = $fields[$group_sort_field]; + if (substr($f, 0, 3) == 'ss_') { + $f = 'sort_' . substr($f, 3); + } + $order = strtolower($order); + $group_params['group.sort'][] = $f . ' ' . $order; + } + } + if (!empty($group_params['group.sort'])) { + $group_params['group.sort'] = implode(', ', $group_params['group.sort']); + } + } + if (!empty($grouping['group_limit']) && ($grouping['group_limit'] != 1)) { + $group_params['group.limit'] = $grouping['group_limit']; + } + } + } + + // Set defaults. + if (!$keys) { + $keys = NULL; + } + + // Collect parameters. + $params = array( + 'fl' => 'item_id,score', + 'qf' => $qf, + 'fq' => $fq, + ); + if (isset($options['offset'])) { + $params['start'] = $options['offset']; + } + $params['rows'] = isset($options['limit']) ? $options['limit'] : 1000000; + if ($sort) { + $params['sort'] = implode(', ', $sort); + } + if (!empty($facet_params['facet.field'])) { + $params += $facet_params; + } + if (!empty($highlight_params)) { + $params += $highlight_params; + } + if (!empty($options['search_api_spellcheck'])) { + $params['spellcheck'] = 'true'; + } + if (!empty($mlt_params['mlt.fl'])) { + $params += $mlt_params; + } + if (!empty($group_params)) { + $params += $group_params; + } + if (!empty($this->options['retrieve_data'])) { + $params['fl'] = '*,score'; + } + // Retrieve http method from server options. + $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO'; + + $call_args = array( + 'query' => &$keys, + 'params' => &$params, + 'http_method' => &$http_method, + ); + if ($this->request_handler) { + $this->setRequestHandler($this->request_handler, $call_args); + } + + try { + // Send search request. + $time_processing_done = microtime(TRUE); + drupal_alter('search_api_solr_query', $call_args, $query); + $this->preQuery($call_args, $query); + + $response = $this->solr->search($keys, $params, $http_method); + $time_query_done = microtime(TRUE); + + // Extract results. + $results = $this->extractResults($query, $response); + + // Add warnings, if present. + if (!empty($warnings)) { + $results['warnings'] = isset($results['warnings']) ? array_merge($warnings, $results['warnings']) : $warnings; + } + + // Extract facets. + if ($facets = $this->extractFacets($query, $response)) { + $results['search_api_facets'] = $facets; + } + + drupal_alter('search_api_solr_search_results', $results, $query, $response); + $this->postQuery($results, $query, $response); + + // Compute performance. + $time_end = microtime(TRUE); + $results['performance'] = array( + 'complete' => $time_end - $time_method_called, + 'preprocessing' => $time_processing_done - $time_method_called, + 'execution' => $time_query_done - $time_processing_done, + 'postprocessing' => $time_end - $time_query_done, + ); + + return $results; + } + catch (SearchApiException $e) { + throw new SearchApiException(t('An error occurred while trying to search with Solr: @msg.', array('@msg' => $e->getMessage()))); + } + } + + /** + * Extract results from a Solr response. + * + * @param object $response + * A HTTP response object. + * + * @return array + * An array with two keys: + * - result count: The number of total results. + * - results: An array of search results, as specified by + * SearchApiQueryInterface::execute(). + */ + protected function extractResults(SearchApiQueryInterface $query, $response) { + $index = $query->getIndex(); + $fields = $this->getFieldNames($index); + $field_options = $index->options['fields']; + + // Set up the results array. + $results = array(); + $results['results'] = array(); + // Keep a copy of the response in the results so it's possible to extract + // further useful information out of it, if necessary. + $results['search_api_solr_response'] = $response; + + // In some rare cases (e.g., MLT query with nonexistent ID) the response + // will be NULL. + if (!isset($response->response) && !isset($response->grouped)) { + $results['result count'] = 0; + return $results; + } + + // If field collapsing has been enabled for this query, we need to process + // the results differently. + $grouping = $query->getOption('search_api_grouping'); + if (!empty($grouping['use_grouping']) && !empty($response->grouped)) { + $docs = array(); + $results['result count'] = 0; + foreach ($grouping['fields'] as $field) { + if (!empty($response->grouped->{$fields[$field]})) { + $results['result count'] += $response->grouped->{$fields[$field]}->ngroups; + foreach ($response->grouped->{$fields[$field]}->groups as $group) { + foreach ($group->doclist->docs as $doc) { + $docs[] = $doc; + } + } + } + } + } + else { + $results['result count'] = $response->response->numFound; + $docs = $response->response->docs; + } + + // Add each search result to the results array. + foreach ($docs as $doc) { + // Blank result array. + $result = array( + 'id' => NULL, + 'score' => NULL, + 'fields' => array(), + ); + + // Extract properties from the Solr document, translating from Solr to + // Search API property names. This reverses the mapping in + // SearchApiSolrService::getFieldNames(). + foreach ($fields as $search_api_property => $solr_property) { + if (isset($doc->{$solr_property})) { + $result['fields'][$search_api_property] = $doc->{$solr_property}; + // Date fields need some special treatment to become valid date values + // (i.e., timestamps) again. + if (isset($field_options[$search_api_property]['type']) + && $field_options[$search_api_property]['type'] == 'date' + && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $result['fields'][$search_api_property])) { + $result['fields'][$search_api_property] = strtotime($result['fields'][$search_api_property]); + } + } + } + + // We can find the item id and score in the special 'search_api_*' + // properties. Mappings are provided for these properties in + // SearchApiSolrService::getFieldNames(). + $result['id'] = $result['fields']['search_api_id']; + $result['score'] = $result['fields']['search_api_relevance']; + + $index_id = $this->getIndexId($index->machine_name); + $solr_id = $this->createId($index_id, $result['id']); + $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields); + if ($excerpt) { + $result['excerpt'] = $excerpt; + } + + // Use the result's id as the array key. By default, 'id' is mapped to + // 'item_id' in SearchApiSolrService::getFieldNames(). + if ($result['id']) { + $results['results'][$result['id']] = $result; + } + } + + // Check for spellcheck suggestions. + if (module_exists('search_api_spellcheck') && $query->getOption('search_api_spellcheck')) { + $results['search_api_spellcheck'] = new SearchApiSpellcheckSolr($response); + } + + return $results; + } + + /** + * Extract and format highlighting information for a specific item from a Solr response. + * + * Will also use highlighted fields to replace retrieved field data, if the + * corresponding option is set. + */ + protected function getExcerpt($response, $id, array &$fields, array $field_mapping) { + if (!isset($response->highlighting->$id)) { + return FALSE; + } + $output = ''; + + if (!empty($this->options['excerpt']) && !empty($response->highlighting->$id->spell)) { + foreach ($response->highlighting->$id->spell as $snippet) { + $snippet = strip_tags($snippet); + $snippet = preg_replace('/^.*>|<.*$/', '', $snippet); + $snippet = $this->formatHighlighting($snippet); + // The created fragments sometimes have leading or trailing punctuation. + // We remove that here for all common cases, but take care not to remove + // < or > (so HTML tags stay valid). + $snippet = trim($snippet, "\00..\x2F:;=\x3F..\x40\x5B..\x60"); + $output .= $snippet . ' … '; + } + } + if (!empty($this->options['highlight_data'])) { + foreach ($field_mapping as $search_api_property => $solr_property) { + if (substr($solr_property, 0, 3) == 'tm_' && !empty($response->highlighting->$id->$solr_property)) { + // Contrary to above, we here want to preserve HTML, so we just + // replace the [HIGHLIGHT] tags with the appropriate format. + $fields[$search_api_property] = $this->formatHighlighting($response->highlighting->$id->$solr_property); + } + } + } + + return $output; + } + + /** + * Changes highlighting tags from our custom, HTML-safe ones to HTML. + * + * @param string|array $snippet + * The snippet(s) to format. + * + * @return string|array + * The snippet(s), properly formatted as HTML. + */ + protected function formatHighlighting($snippet) { + return preg_replace('#\[(/?)HIGHLIGHT\]#', '<$1strong>', $snippet); + } + + /** + * Extract facets from a Solr response. + * + * @param object $response + * A response object from SolrPhpClient. + * + * @return array + * An array describing facets that apply to the current results. + */ + protected function extractFacets(SearchApiQueryInterface $query, $response) { + $facets = array(); + + if (!isset($response->facet_counts)) { + return $facets; + } + + $index = $query->getIndex(); + $fields = $this->getFieldNames($index); + + $extract_facets = $query->getOption('search_api_facets', array()); + + if (isset($response->facet_counts->facet_fields)) { + $facet_fields = $response->facet_counts->facet_fields; + + foreach ($extract_facets as $delta => $info) { + $field = $fields[$info['field']]; + if (!empty($facet_fields->$field)) { + $min_count = $info['min_count']; + $terms = $facet_fields->$field; + if ($info['missing']) { + // We have to correctly incorporate the "_empty_" term. + // This will ensure that the term with the least results is dropped, + // if the limit would be exceeded. + if (isset($terms->_empty_)) { + if ($terms->_empty_ < $min_count) { + unset($terms->_empty_); + } + else { + $terms = (array) $terms; + arsort($terms); + if ($info['limit'] > 0 && count($terms) > $info['limit']) { + array_pop($terms); + } + } + } + } + elseif (isset($terms->_empty_)) { + $terms = clone $terms; + unset($terms->_empty_); + } + $type = isset($index->options['fields'][$info['field']]['type']) ? search_api_extract_inner_type($index->options['fields'][$info['field']]['type']) : 'string'; + foreach ($terms as $term => $count) { + if ($count >= $min_count) { + if ($term === '_empty_') { + $term = '!'; + } + elseif ($type == 'boolean') { + if ($term == 'true') { + $term = '"1"'; + } + elseif ($term == 'false') { + $term = '"0"'; + } + } + elseif ($type == 'date') { + $term = $term ? '"' . strtotime($term) . '"' : NULL; + } + else { + $term = "\"$term\""; + } + if ($term) { + $facets[$delta][] = array( + 'filter' => $term, + 'count' => $count, + ); + } + } + } + if (empty($facets[$delta])) { + unset($facets[$delta]); + } + } + } + + } + + if (isset($response->facet_counts->facet_queries)) { + if ($spatials = $query->getOption('search_api_location')) { + $queries = array(); + foreach ($response->facet_counts->facet_queries as $key => $count) { + if (!preg_match('/^spatial-(.*)-(\d+(?:\.\d+)?)$/', $key, $m)) { + continue; + } + if (empty($extract_facets[$m[1]])) { + continue; + } + $facet = $extract_facets[$m[1]]; + if ($count >= $facet['min_count']) { + $facets[$m[1]][] = array( + 'filter' => "[* {$m[2]}]", + 'count' => $count, + ); + } + } + } + } + + return $facets; + } + + /** + * Flatten a keys array into a single search string. + * + * @param array $keys + * The keys array to flatten, formatted as specified by + * SearchApiQueryInterface::getKeys(). + * + * @return string + * A Solr query string representing the same keys. + */ + protected function flattenKeys(array $keys) { + $k = array(); + $or = $keys['#conjunction'] == 'OR'; + $neg = !empty($keys['#negation']); + foreach (element_children($keys) as $i) { + $key = $keys[$i]; + if (!$key) { + continue; + } + if (is_array($key)) { + $subkeys = $this->flattenKeys($key); + if ($subkeys) { + $nested_expressions = TRUE; + // If this is a negated OR expression, we can't just use nested keys + // as-is, but have to put them into parantheses. + if ($or && $neg) { + $subkeys = "($subkeys)"; + } + $k[] = $subkeys; + } + } + else { + $key = trim($key); + $key = call_user_func(array($this->connection_class, 'phrase'), $key); + $k[] = $key; + } + } + if (!$k) { + return ''; + } + + // Formatting the keys into a Solr query can be a bit complex. The following + // code will produce filters that look like this: + // + // #conjunction | #negation | return value + // ---------------------------------------------------------------- + // AND | FALSE | A B C + // AND | TRUE | -(A AND B AND C) + // OR | FALSE | ((A) OR (B) OR (C)) + // OR | TRUE | -A -B -C + + // If there was just a single, unnested key, we can ignore all this. + if (count($k) == 1 && empty($nested_expressions)) { + $k = reset($k); + return $neg ? "*:* AND -$k" : $k; + } + + if ($or) { + if ($neg) { + return '*:* AND -' . implode(' AND -', $k); + } + return '((' . implode(') OR (', $k) . '))'; + } + $k = implode($neg ? ' AND ' : ' ', $k); + return $neg ? "*:* AND -($k)" : $k; + } + + /** + * Transforms a query filter into a flat array of Solr filter queries, using + * the field names in $fields. + */ + protected function createFilterQueries(SearchApiQueryFilterInterface $filter, array $solr_fields, array $fields) { + $or = $filter->getConjunction() == 'OR'; + $fq = array(); + foreach ($filter->getFilters() as $f) { + if (is_array($f)) { + if (!isset($fields[$f[0]])) { + throw new SearchApiException(t('Filter term on unknown or unindexed field @field.', array('@field' => $f[0]))); + } + if ($f[1] !== '') { + $fq[] = $this->createFilterQuery($solr_fields[$f[0]], $f[1], $f[2], $fields[$f[0]]); + } + } + else { + $q = $this->createFilterQueries($f, $solr_fields, $fields); + if ($filter->getConjunction() != $f->getConjunction()) { + // $or == TRUE means the nested filter has conjunction AND, and vice versa + $sep = $or ? ' ' : ' OR '; + $fq[] = count($q) == 1 ? reset($q) : '((' . implode(')' . $sep . '(', $q) . '))'; + } + else { + $fq = array_merge($fq, $q); + } + } + } + return ($or && count($fq) > 1) ? array('((' . implode(') OR (', $fq) . '))') : $fq; + } + + /** + * Create a single search query string according to the given field, value + * and operator. + */ + protected function createFilterQuery($field, $value, $operator, $field_info) { + $field = call_user_func(array($this->connection_class, 'escapeFieldName'), $field); + if ($value === NULL) { + return ($operator == '=' ? '*:* AND -' : '') . "$field:[* TO *]"; + } + $value = trim($value); + $value = $this->formatFilterValue($value, search_api_extract_inner_type($field_info['type'])); + switch ($operator) { + case '<>': + return "*:* AND -($field:$value)"; + case '<': + return "$field:{* TO $value}"; + case '<=': + return "$field:[* TO $value]"; + case '>=': + return "$field:[$value TO *]"; + case '>': + return "$field:{{$value} TO *}"; + + default: + return "$field:$value"; + } + } + + /** + * Format a value for filtering on a field of a specific type. + */ + protected function formatFilterValue($value, $type) { + switch ($type) { + case 'boolean': + $value = $value ? 'true' : 'false'; + break; + case 'date': + $value = is_numeric($value) ? (int) $value : strtotime($value); + if ($value === FALSE) { + return 0; + } + $value = format_date($value, 'custom', self::SOLR_DATE_FORMAT, 'UTC'); + break; + } + return call_user_func(array($this->connection_class, 'phrase'), $value); + } + + /** + * Helper method for creating the facet field parameters. + */ + protected function getFacetParams(array $facets, array $fields, array &$fq = array()) { + if (!$facets) { + return array(); + } + $facet_params['facet'] = 'true'; + $facet_params['facet.sort'] = 'count'; + $facet_params['facet.limit'] = 10; + $facet_params['facet.mincount'] = 1; + $facet_params['facet.missing'] = 'false'; + $taggedFields = array(); + foreach ($facets as $info) { + if (empty($fields[$info['field']])) { + continue; + } + // String fields have their own corresponding facet fields. + $field = $fields[$info['field']]; + // Check for the "or" operator. + if (isset($info['operator']) && $info['operator'] === 'or') { + // Remember that filters for this field should be tagged. + $escaped = call_user_func(array($this->connection_class, 'escapeFieldName'), $fields[$info['field']]); + $taggedFields[$escaped] = "{!tag=$escaped}"; + // Add the facet field. + $facet_params['facet.field'][] = "{!ex=$escaped}$field"; + } + else { + // Add the facet field. + $facet_params['facet.field'][] = $field; + } + // Set limit, unless it's the default. + if ($info['limit'] != 10) { + $facet_params["f.$field.facet.limit"] = $info['limit'] ? $info['limit'] : -1; + } + // Set mincount, unless it's the default. + if ($info['min_count'] != 1) { + $facet_params["f.$field.facet.mincount"] = $info['min_count']; + } + // Set missing, if specified. + if ($info['missing']) { + $facet_params["f.$field.facet.missing"] = 'true'; + } + } + // Tag filters of fields with "OR" facets. + foreach ($taggedFields as $field => $tag) { + $regex = '#(? $filter) { + // Solr can't handle two tags on the same filter, so we don't add two. + // Another option here would even be to remove the other tag, too, + // since we can be pretty sure that this filter does not originate from + // a facet – however, wrong results would still be possible, and this is + // definitely an edge case, so don't bother. + if (preg_match($regex, $filter) && substr($filter, 0, 6) != '{!tag=') { + $fq[$i] = $tag . $filter; + } + } + } + + return $facet_params; + } + + /** + * Helper method for creating the highlighting parameters. + * + * (The $query parameter currently isn't used and only here for the potential + * sake of subclasses.) + * + * @param SearchApiQueryInterface|SearchApiMultiQueryInterface $query + * The query object, either for a normal Search API query or a multi-index + * query. + * + * @return array + * An array of parameters to be added to the Solr search request. + */ + protected function getHighlightParams($query) { + $highlight_params = array(); + + if (!empty($this->options['excerpt']) || !empty($this->options['highlight_data'])) { + $highlight_params['hl'] = 'true'; + $highlight_params['hl.fl'] = 'spell'; + $highlight_params['hl.simple.pre'] = '[HIGHLIGHT]'; + $highlight_params['hl.simple.post'] = '[/HIGHLIGHT]'; + $highlight_params['hl.snippets'] = 3; + $highlight_params['hl.fragsize'] = 70; + $highlight_params['hl.mergeContiguous'] = 'true'; + } + + if (!empty($this->options['highlight_data'])) { + $highlight_params['hl.fl'] = 'tm_*'; + $highlight_params['hl.snippets'] = 1; + $highlight_params['hl.fragsize'] = 0; + if (!empty($this->options['excerpt'])) { + // If we also generate a "normal" excerpt, set the settings for the + // "spell" field (which we use to generate the excerpt) back to the + // above values. + $highlight_params['f.spell.hl.snippets'] = 3; + $highlight_params['f.spell.hl.fragsize'] = 70; + // It regrettably doesn't seem to be possible to set hl.fl to several + // values, if one contains wild cards (i.e., "t_*,spell" wouldn't work). + $highlight_params['hl.fl'] = '*'; + } + } + + return $highlight_params; + } + + /** + * Sets the request handler. + * + * This should also make the needed adjustments to the request parameters. + * + * @param $handler + * Name of the handler to set. + * @param array $call_args + * An associative array containing all three arguments to the + * SearchApiSolrConnectionInterface::search() call ("query", "params" and + * "method") as references. + * + * @return bool + * TRUE iff this method invocation handled the given handler. This allows + * subclasses to recognize whether the request handler was already set by + * this method. + */ + protected function setRequestHandler($handler, array &$call_args) { + if ($handler == 'pinkPony') { + $call_args['params']['qt'] = $handler; + return TRUE; + } + return FALSE; + } + + /** + * Empty method called before sending a search query to Solr. + * + * This allows subclasses to apply custom changes before the query is sent to + * Solr. Works exactly like hook_search_api_solr_query_alter(). + * + * @param array $call_args + * An associative array containing all three arguments to the + * SearchApiSolrConnectionInterface::search() call ("query", "params" and + * "method") as references. + * @param SearchApiQueryInterface $query + * The SearchApiQueryInterface object representing the executed search query. + */ + protected function preQuery(array &$call_args, SearchApiQueryInterface $query) { + } + + /** + * Empty method to allow subclasses to apply custom changes before search results are returned. + * + * Works exactly like hook_search_api_solr_search_results_alter(). + * + * @param array $results + * The results array that will be returned for the search. + * @param SearchApiQueryInterface $query + * The SearchApiQueryInterface object representing the executed search query. + * @param object $response + * The response object returned by Solr. + */ + protected function postQuery(array &$results, SearchApiQueryInterface $query, $response) { + } + + // + // Autocompletion feature + // + + /** + * Implements SearchApiAutocompleteInterface::getAutocompleteSuggestions(). + */ + // Largely copied from the apachesolr_autocomplete module. + public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) { + $suggestions = array(); + // Reset request handler + $this->request_handler = NULL; + // Turn inputs to lower case, otherwise we get case sensivity problems. + $incomp = drupal_strtolower($incomplete_key); + + $index = $query->getIndex(); + $fields = $this->getFieldNames($index); + $complete = $query->getOriginalKeys(); + + // Extract keys + $keys = $query->getKeys(); + if (is_array($keys)) { + $keys_array = array(); + while ($keys) { + reset($keys); + if (!element_child(key($keys))) { + array_shift($keys); + continue; + } + $key = array_shift($keys); + if (is_array($key)) { + $keys = array_merge($keys, $key); + } + else { + $keys_array[$key] = $key; + } + } + $keys = $this->flattenKeys($query->getKeys()); + } + else { + $keys_array = drupal_map_assoc(preg_split('/[-\s():{}\[\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY)); + } + if (!$keys) { + $keys = NULL; + } + + // Set searched fields + $options = $query->getOptions(); + $search_fields = $query->getFields(); + $qf = array(); + foreach ($search_fields as $f) { + $qf[] = $fields[$f]; + } + + // Extract filters + $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']); + $fq[] = 'index_id:' . $this->getIndexId($index->machine_name); + + // Autocomplete magic + $facet_fields = array(); + foreach ($search_fields as $f) { + $facet_fields[] = $fields[$f]; + } + + $limit = $query->getOption('limit', 10); + + $params = array( + 'qf' => $qf, + 'fq' => $fq, + 'rows' => 0, + 'facet' => 'true', + 'facet.field' => $facet_fields, + 'facet.prefix' => $incomp, + 'facet.limit' => $limit * 5, + 'facet.mincount' => 1, + 'spellcheck' => (!isset($this->options['autocorrect_spell']) || $this->options['autocorrect_spell']) ? 'true' : 'false', + 'spellcheck.count' => 1, + ); + // Retrieve http method from server options. + $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO'; + + $call_args = array( + 'query' => &$keys, + 'params' => &$params, + 'http_method' => &$http_method, + ); + if ($this->request_handler) { + $this->setRequestHandler($this->request_handler, $call_args); + } + $second_pass = !isset($this->options['autocorrect_suggest_words']) || $this->options['autocorrect_suggest_words']; + for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) { + try { + // Send search request + $this->connect(); + drupal_alter('search_api_solr_query', $call_args, $query); + $this->preQuery($call_args, $query); + $response = $this->solr->search($keys, $params, $http_method); + + if (!empty($response->spellcheck->suggestions)) { + $replace = array(); + foreach ($response->spellcheck->suggestions as $word => $data) { + $replace[$word] = $data->suggestion[0]; + } + $corrected = str_ireplace(array_keys($replace), array_values($replace), $user_input); + if ($corrected != $user_input) { + array_unshift($suggestions, array( + 'prefix' => t('Did you mean') . ':', + 'user_input' => $corrected, + )); + } + } + + $matches = array(); + if (isset($response->facet_counts->facet_fields)) { + foreach ($response->facet_counts->facet_fields as $terms) { + foreach ($terms as $term => $count) { + if (isset($matches[$term])) { + // If we just add the result counts, we can easily get over the + // total number of results if terms appear in multiple fields. + // Therefore, we just take the highest value from any field. + $matches[$term] = max($matches[$term], $count); + } + else { + $matches[$term] = $count; + } + } + } + + if ($matches) { + // Eliminate suggestions that are too short or already in the query. + foreach ($matches as $term => $count) { + if (strlen($term) < 3 || isset($keys_array[$term])) { + unset($matches[$term]); + } + } + + // Don't suggest terms that are too frequent (by default in more + // than 90% of results). + $result_count = $response->response->numFound; + $max_occurrences = $result_count * variable_get('search_api_solr_autocomplete_max_occurrences', 0.9); + if (($max_occurrences >= 1 || $i > 0) && $max_occurrences < $result_count) { + foreach ($matches as $match => $count) { + if ($count > $max_occurrences) { + unset($matches[$match]); + } + } + } + + // The $count in this array is actually a score. We want the + // highest ones first. + arsort($matches); + + // Shorten the array to the right ones. + $additional_matches = array_slice($matches, $limit - count($suggestions), NULL, TRUE); + $matches = array_slice($matches, 0, $limit, TRUE); + + // Build suggestions using returned facets + $incomp_length = strlen($incomp); + foreach ($matches as $term => $count) { + if (drupal_strtolower(substr($term, 0, $incomp_length)) == $incomp) { + $suggestions[] = array( + 'suggestion_suffix' => substr($term, $incomp_length), + 'term' => $term, + 'results' => $count, + ); + } + else { + $suggestions[] = array( + 'suggestion_suffix' => ' ' . $term, + 'term' => $term, + 'results' => $count, + ); + } + } + } + } + } + catch (SearchApiException $e) { + watchdog_exception('search_api_solr', $e, "%type during autocomplete Solr query: !message in %function (line %line of %file).", array(), WATCHDOG_WARNING); + } + + if (count($suggestions) >= $limit) { + break; + } + // Change parameters for second query. + unset($params['facet.prefix']); + $keys = trim ($keys . ' ' . $incomplete_key); + } + + return $suggestions; + } + + // + // SearchApiMultiServiceInterface methods + // + + /** + * Implements SearchApiMultiServiceInterface::queryMultiple(). + */ + public function queryMultiple(array $options = array()) { + return new SearchApiMultiQuery($this->server, $options); + } + + /** + * Implements SearchApiMultiServiceInterface::searchMultiple(). + */ + public function searchMultiple(SearchApiMultiQueryInterface $query) { + $time_method_called = microtime(TRUE); + // Get field information + $solr_fields = array( + 'search_api_id' => 'item_id', + 'search_api_relevance' => 'score', + 'search_api_multi_index' => 'index_id', + ); + $fields = array( + 'search_api_multi_index' => array( + 'type' => 'string', + ), + ); + foreach ($query->getIndexes() as $index) { + if (empty($index->options['fields'])) { + continue; + } + $prefix = $this->getIndexId($index->machine_name) . ':'; + foreach ($this->getFieldNames($index) as $field => $key) { + if (!isset($solr_fields[$field])) { + $solr_fields[$prefix . $field] = $key; + } + } + foreach ($index->options['fields'] as $field => $info) { + $fields[$prefix . $field] = $info; + } + } + + // Extract keys + $keys = $query->getKeys(); + if (is_array($keys)) { + $keys = $this->flattenKeys($keys); + } + + // Set searched fields + $search_fields = $query->getFields(); + $qf = array(); + foreach ($search_fields as $f) { + $qf[] = $solr_fields[$f]; + } + + // Extract filters + $filter = $query->getFilter(); + $fq = $this->createFilterQueries($filter, $solr_fields, $fields); + + // Restrict search to searched indexes. + $index_filter = array(); + foreach ($query->getIndexes() as $index) { + $index_id = $this->getIndexId($index->machine_name); + $index_filter[] = 'index_id:' . call_user_func(array($this->connection_class, 'phrase'), $index_id); + } + $fq[] = implode(' OR ', $index_filter); + + // Extract sort + $sort = array(); + foreach ($query->getSort() as $f => $order) { + $f = $solr_fields[$f]; + if (substr($f, 0, 3) == 'ss_') { + $f = 'sort_' . substr($f, 3); + } + $order = strtolower($order); + $sort[] = "$f $order"; + } + + // Get facet fields + $facets = $query->getOption('search_api_facets') ? $query->getOption('search_api_facets') : array(); + $facet_params = $this->getFacetParams($facets, $solr_fields, $fq); + + // Handle highlighting. + $highlight_params = $this->getHighlightParams($query); + + // Set defaults + if (!$keys) { + $keys = NULL; + } + $options = $query->getOptions(); + + // Collect parameters + $params = array( + 'fl' => 'item_id,index_id,score', + 'qf' => $qf, + 'fq' => $fq, + ); + if (isset($options['offset'])) { + $params['start'] = $options['offset']; + } + if (isset($options['limit'])) { + $params['rows'] = $options['limit']; + } + if ($sort) { + $params['sort'] = implode(', ', $sort); + } + if (!empty($facet_params['facet.field'])) { + $params += $facet_params; + } + if (!empty($highlight_params)) { + $params += $highlight_params; + } + + // Retrieve http method from server options. + $http_method = !empty($this->options['http_method']) ? $this->options['http_method'] : 'AUTO'; + + // Send search request + $time_processing_done = microtime(TRUE); + $this->connect(); + + $call_args = array( + 'query' => &$keys, + 'params' => &$params, + 'http_method' => &$http_method, + ); + drupal_alter('search_api_solr_multi_query', $call_args, $query); + + $response = $this->solr->search($keys, $params, $http_method); + $time_query_done = microtime(TRUE); + + // Extract results + $results = array(); + $results['result count'] = $response->response->numFound; + $results['results'] = array(); + $tmp = array(); + foreach ($response->response->docs as $id => $doc) { + $result = array( + 'id' => $doc->item_id, + 'index_id' => $doc->index_id, + 'score' => $doc->score, + ); + $solr_id = $this->createId($doc->index_id, $result['id']); + $excerpt = $this->getExcerpt($response, $solr_id, $tmp, array()); + if ($excerpt) { + $result['excerpt'] = $excerpt; + } + $results['results'][$id] = $result; + } + + // Extract facets + if (isset($response->facet_counts->facet_fields)) { + $results['search_api_facets'] = array(); + $facet_fields = $response->facet_counts->facet_fields; + foreach ($facets as $delta => $info) { + $field = $solr_fields[$info['field']]; + if (!empty($facet_fields->$field)) { + $min_count = $info['min_count']; + $terms = $facet_fields->$field; + if ($info['missing']) { + // We have to correctly incorporate the "_empty_" term. + // This will ensure that the term with the least results is dropped, + // if the limit would be exceeded. + if (isset($terms->_empty_)) { + if ($terms->_empty_ < $min_count) { + unset($terms->_empty_); + } + else { + $terms = (array) $terms; + arsort($terms); + if ($info['limit'] > 0 && count($terms) > $info['limit']) { + array_pop($terms); + } + } + } + } + elseif (isset($terms->_empty_)) { + $terms = clone $terms; + unset($terms->_empty_); + } + $type = isset($fields[$info['field']]['type']) ? search_api_extract_inner_type($fields[$info['field']]['type']) : 'string'; + foreach ($terms as $term => $count) { + if ($count >= $min_count) { + if ($term === '_empty_') { + $term = '!'; + } + elseif ($type == 'boolean') { + if ($term == 'true') { + $term = '"1"'; + } + elseif ($term == 'false') { + $term = '"0"'; + } + } + elseif ($type == 'date') { + $term = $term ? '"' . strtotime($term) . '"' : NULL; + } + else { + $term = "\"$term\""; + } + if ($term) { + $results['search_api_facets'][$delta][] = array( + 'filter' => $term, + 'count' => $count, + ); + } + } + } + if (empty($results['search_api_facets'][$delta])) { + unset($results['search_api_facets'][$delta]); + } + } + } + } + + // Compute performance + $time_end = microtime(TRUE); + $results['performance'] = array( + 'complete' => $time_end - $time_method_called, + 'preprocessing' => $time_processing_done - $time_method_called, + 'execution' => $time_query_done - $time_processing_done, + 'postprocessing' => $time_end - $time_query_done, + ); + + return $results; + } + + // + // Additional methods that might be used when knowing the service class. + // + + /** + * Ping the Solr server to tell whether it can be accessed. + * + * Uses the admin/ping request handler. + */ + public function ping() { + $this->connect(); + return $this->solr->ping(); + } + + /** + * Sends a commit command to the Solr server. + */ + public function commit() { + try { + $this->connect(); + return $this->solr->commit(FALSE); + } + catch (SearchApiException $e) { + watchdog_exception('search_api_solr', $e, + '%type while trying to commit on server @server: !message in %function (line %line of %file).', + array('@server' => $this->server->machine_name), WATCHDOG_WARNING); + } + } + + /** + * Schedules a commit operation for this server. + * + * The commit will be sent at the end of the current page request. Multiple + * calls to this method will still only result in one commit operation. + */ + public function scheduleCommit() { + if (!$this->commitScheduled) { + $this->commitScheduled = TRUE; + drupal_register_shutdown_function(array($this, 'commit')); + } + } + + /** + * Gets the Solr connection class used by this service. + * + * @return string + * The name of a class which implements SearchApiSolrConnectionInterface. + */ + public function getConnectionClass() { + return $this->connection_class; + } + + /** + * Sets the Solr connection class used by this service. + * + * @param string $class + * The name of a class which implements SearchApiSolrConnectionInterface. + */ + public function setConnectionClass($class) { + $this->connection_class = $class; + $this->solr = NULL; + } + + /** + * Gets the currently used Solr connection object. + * + * @return SearchApiSolrConnectionInterface + * The solr connection object used by this server. + */ + public function getSolrConnection() { + $this->connect(); + return $this->solr; + } + + /** + * Get metadata about fields in the Solr/Lucene index. + * + * @param int $num_terms + * Number of 'top terms' to return. + * + * @return array + * An array of SearchApiSolrField objects. + * + * @see SearchApiSolrConnectionInterface::getFields() + */ + public function getFields($num_terms = 0) { + $this->connect(); + return $this->solr->getFields($num_terms); + } + + /** + * Retrieves a config file or file list from the Solr server. + * + * Uses the admin/file request handler. + * + * @param string|null $file + * (optional) The name of the file to retrieve. If the file is a directory, + * the directory contents are instead listed and returned. NULL represents + * the root config directory. + * + * @return object + * A HTTP response object containing either the file contents or a file list. + */ + public function getFile($file = NULL) { + $this->connect(); + + $file_servlet_name = constant($this->connection_class . '::FILE_SERVLET'); + + $params['contentType'] = 'text/xml;charset=utf-8'; + if ($file) { + $params['file'] = $file; + } + return $this->solr->makeServletRequest($file_servlet_name, $params); + } + + /** + * Prefixes an index ID as configured. + * + * The resulting ID will be a concatenation of the following strings: + * - If set, the "search_api_solr_index_prefix" variable. + * - If set, the index-specific "search_api_solr_index_prefix_INDEX" variable. + * - The index's machine name. + * + * @param string $machine_name + * The index's machine name. + * + * @return string + * The prefixed machine name. + */ + protected function getIndexId($machine_name) { + // Prepend per-index prefix. + $id = variable_get('search_api_solr_index_prefix_' . $machine_name, '') . $machine_name; + // Prepend environment prefix. + $id = variable_get('search_api_solr_index_prefix', '') . $id; + return $id; + } +} diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc b/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc new file mode 100644 index 00000000..6b996471 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.inc @@ -0,0 +1,905 @@ + + */ + +/** + * Additional code Copyright (c) 2008-2011 by Robert Douglass, James McKinney, + * Jacob Singh, Alejandro Garza, Peter Wolanin, and additional contributors. + * + * 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. + */ + +/** + * Represents a Solr server resource. + * + * Contains methods for pinging, adding, deleting, committing, optimizing and + * searching. + */ +class SearchApiSolrConnection implements SearchApiSolrConnectionInterface { + + /** + * Defines how NamedLists should be formatted in the output. + * + * This specifically affects facet counts. Valid values are 'map' (default) or + * 'flat'. + */ + const NAMED_LIST_FORMAT = 'map'; + + /** + * Path to the ping servlet. + */ + const PING_SERVLET = 'admin/ping'; + + /** + * Path to the update servlet. + */ + const UPDATE_SERVLET = 'update'; + + /** + * Path to the search servlet. + */ + const SEARCH_SERVLET = 'select'; + + /** + * Path to the luke servlet. + */ + const LUKE_SERVLET = 'admin/luke'; + + /** + * Path to the system servlet. + */ + const SYSTEM_SERVLET = 'admin/system'; + + /** + * Path to the stats servlet. + */ + const STATS_SERVLET = 'admin/stats.jsp'; + + /** + * Path to the stats servlet for Solr 4.x servers. + */ + const STATS_SERVLET_4 = 'admin/mbeans?wt=xml&stats=true'; + + /** + * Path to the file servlet. + */ + const FILE_SERVLET = 'admin/file'; + + /** + * The options passed when creating this connection. + * + * @var array + */ + protected $options; + + /** + * The Solr server's URL. + * + * @var string + */ + protected $base_url; + + /** + * Cached URL to the update servlet. + * + * @var string + */ + protected $update_url; + + /** + * HTTP Basic Authentication header to set for requests to the Solr server. + * + * @var string + */ + protected $http_auth; + + /** + * The stream context to use for requests to the Solr server. + * + * Defaults to NULL (= pass no context at all). + * + * @var string + */ + protected $stream_context; + + /** + * Cache for the metadata from admin/luke. + * + * Contains an array of response objects, keyed by the number of "top terms". + * + * @var array + * + * @see getLuke() + */ + protected $luke = array(); + + /** + * Cache for information about the Solr core. + * + * @var SimpleXMLElement + * + * @see getStats() + */ + protected $stats; + + /** + * Cache for system information. + * + * @var array + * + * @see getSystemInfo() + */ + protected $system_info; + + /** + * Flag that denotes whether to use soft commits for Solr 4.x. + * + * Defaults to FALSE. + * + * @var bool + */ + protected $soft_commit = FALSE; + + /** + * Implements SearchApiSolrConnectionInterface::__construct(). + * + * Valid options include: + * - scheme: Scheme of the base URL of the Solr server. Most probably "http" + * or "https". Defaults to "http". + * - host: The host name (or IP) of the Solr server. Defaults to + * "localhost". + * - port: The port of the Solr server. Defaults to 8983. + * - path: The base path to the Solr server. Defaults to "/solr/". + * - http_user: If both this and "http_pass" are set, will use this + * information to add basic HTTP authentication to all requests to the + * Solr server. Not set by default. + * - http_pass: See "http_user". + */ + public function __construct(array $options) { + $options += array( + 'scheme' => 'http', + 'host' => 'localhost', + 'port' => 8983, + 'path' => 'solr', + 'http_user' => NULL, + 'http_pass' => NULL, + ); + $this->options = $options; + + $path = '/' . trim($options['path'], '/') . '/'; + $this->base_url = $options['scheme'] . '://' . $options['host'] . ':' . $options['port'] . $path; + + // Set HTTP Basic Authentication parameter, if login data was set. + if (strlen($options['http_user']) && strlen($options['http_pass'])) { + $this->http_auth = 'Basic ' . base64_encode($options['http_user'] . ':' . $options['http_pass']); + } + } + + /** + * Implements SearchApiSolrConnectionInterface::ping(). + */ + public function ping($timeout = 2) { + $start = microtime(TRUE); + + if ($timeout <= 0.0) { + $timeout = -1; + } + $pingUrl = $this->constructUrl(self::PING_SERVLET); + + // Attempt a HEAD request to the Solr ping url. + $options = array( + 'method' => 'HEAD', + 'timeout' => $timeout, + ); + $response = $this->makeHttpRequest($pingUrl, $options); + + if ($response->code == 200) { + // Add 1 µs to the ping time so we never return 0. + return (microtime(TRUE) - $start) + 1E-6; + } + else { + return FALSE; + } + } + + /** + * Implements SearchApiSolrConnectionInterface::setSoftCommit(). + */ + public function setSoftCommit($soft_commit) { + $this->soft_commit = (bool) $soft_commit; + } + + /** + * Implements SearchApiSolrConnectionInterface::getSoftCommit(). + */ + public function getSoftCommit() { + return $this->soft_commit; + } + + /** + * Implements SearchApiSolrConnectionInterface::setStreamContext(). + */ + public function setStreamContext($stream_context) { + $this->stream_context = $stream_context; + } + + /** + * Implements SearchApiSolrConnectionInterface::getStreamContext(). + */ + public function getStreamContext() { + return $this->stream_context; + } + + /** + * Computes the cache ID to use for this connection. + * + * @param $suffix + * (optional) A suffix to append to the string to make it unique. + * + * @return string|null + * The cache ID to use for this connection and usage; or NULL if no caching + * should take place. + */ + protected function getCacheId($suffix = '') { + if (!empty($this->options['server'])) { + $cid = $this->options['server']; + return $suffix ? "$cid:$suffix" : $cid; + } + } + + /** + * Call the /admin/system servlet to retrieve system information. + * + * Stores the retrieved information in $system_info. + * + * @see getSystemInfo() + */ + protected function setSystemInfo() { + $cid = $this->getCacheId(__FUNCTION__); + if ($cid) { + $cache = cache_get($cid, 'cache_search_api_solr'); + if ($cache) { + $this->system_info = json_decode($cache->data); + } + } + // Second pass to populate the cache if necessary. + if (empty($this->system_info)) { + $url = $this->constructUrl(self::SYSTEM_SERVLET, array('wt' => 'json')); + $response = $this->sendRawGet($url); + $this->system_info = json_decode($response->data); + if ($cid) { + cache_set($cid, $response->data, 'cache_search_api_solr'); + } + } + } + + /** + * Implements SearchApiSolrConnectionInterface::getSystemInfo(). + */ + public function getSystemInfo() { + if (!isset($this->system_info)) { + $this->setSystemInfo(); + } + return $this->system_info; + } + + /** + * Sets $this->luke with the metadata about the index from admin/luke. + */ + protected function setLuke($num_terms = 0) { + if (empty($this->luke[$num_terms])) { + $cid = $this->getCacheId(__FUNCTION__ . ":$num_terms"); + if ($cid) { + $cache = cache_get($cid, 'cache_search_api_solr'); + if (isset($cache->data)) { + $this->luke = $cache->data; + } + } + // Second pass to populate the cache if necessary. + if (empty($this->luke[$num_terms])) { + $params = array( + 'numTerms' => "$num_terms", + 'wt' => 'json', + 'json.nl' => self::NAMED_LIST_FORMAT, + ); + $url = $this->constructUrl(self::LUKE_SERVLET, $params); + $this->luke[$num_terms] = $this->sendRawGet($url); + if ($cid) { + cache_set($cid, $this->luke, 'cache_search_api_solr'); + } + } + } + } + + /** + * Implements SearchApiSolrConnectionInterface::getFields(). + */ + public function getFields($num_terms = 0) { + $fields = array(); + foreach ($this->getLuke($num_terms)->fields as $name => $info) { + $fields[$name] = new SearchApiSolrField($info); + } + return $fields; + } + + /** + * Implements SearchApiSolrConnectionInterface::getLuke(). + */ + public function getLuke($num_terms = 0) { + if (!isset($this->luke[$num_terms])) { + $this->setLuke($num_terms); + } + return $this->luke[$num_terms]; + } + + /** + * Implements SearchApiSolrConnectionInterface::getSolrVersion(). + */ + public function getSolrVersion() { + $system_info = $this->getSystemInfo(); + // Get our solr version number + if (isset($system_info->lucene->{'solr-spec-version'})) { + return $system_info->lucene->{'solr-spec-version'}[0]; + } + return 0; + } + + /** + * Stores information about the Solr core in $this->stats. + */ + protected function setStats() { + $data = $this->getLuke(); + $solr_version = $this->getSolrVersion(); + // Only try to get stats if we have connected to the index. + if (empty($this->stats) && isset($data->index->numDocs)) { + $cid = $this->getCacheId(__FUNCTION__); + if ($cid) { + $cache = cache_get($cid, 'cache_search_api_solr'); + if (isset($cache->data)) { + $this->stats = simplexml_load_string($cache->data); + } + } + // Second pass to populate the cache if necessary. + if (empty($this->stats)) { + if ($solr_version >= 4) { + $url = $this->constructUrl(self::STATS_SERVLET_4); + } + else { + $url = $this->constructUrl(self::STATS_SERVLET); + } + $response = $this->sendRawGet($url); + $this->stats = simplexml_load_string($response->data); + if ($cid) { + cache_set($cid, $response->data, 'cache_search_api_solr'); + } + } + } + } + + /** + * Implements SearchApiSolrConnectionInterface::getStats(). + */ + public function getStats() { + if (!isset($this->stats)) { + $this->setStats(); + } + return $this->stats; + } + + /** + * Implements SearchApiSolrConnectionInterface::getStatsSummary(). + */ + public function getStatsSummary() { + $stats = $this->getStats(); + $solr_version = $this->getSolrVersion(); + + $summary = array( + '@pending_docs' => '', + '@autocommit_time_seconds' => '', + '@autocommit_time' => '', + '@deletes_by_id' => '', + '@deletes_by_query' => '', + '@deletes_total' => '', + '@schema_version' => '', + '@core_name' => '', + '@index_size' => '', + ); + + if (!empty($stats)) { + if ($solr_version <= 3) { + $docs_pending_xpath = $stats->xpath('//stat[@name="docsPending"]'); + $summary['@pending_docs'] = (int) trim(current($docs_pending_xpath)); + $max_time_xpath = $stats->xpath('//stat[@name="autocommit maxTime"]'); + $max_time = (int) trim(current($max_time_xpath)); + // Convert to seconds. + $summary['@autocommit_time_seconds'] = $max_time / 1000; + $summary['@autocommit_time'] = format_interval($max_time / 1000); + $deletes_id_xpath = $stats->xpath('//stat[@name="deletesById"]'); + $summary['@deletes_by_id'] = (int) trim(current($deletes_id_xpath)); + $deletes_query_xpath = $stats->xpath('//stat[@name="deletesByQuery"]'); + $summary['@deletes_by_query'] = (int) trim(current($deletes_query_xpath)); + $summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query']; + $schema = $stats->xpath('/solr/schema[1]'); + $summary['@schema_version'] = trim($schema[0]); + $core = $stats->xpath('/solr/core[1]'); + $summary['@core_name'] = trim($core[0]); + $size_xpath = $stats->xpath('//stat[@name="indexSize"]'); + $summary['@index_size'] = trim(current($size_xpath)); + } + else { + $system_info = $this->getSystemInfo(); + $docs_pending_xpath = $stats->xpath('//lst["stats"]/long[@name="docsPending"]'); + $summary['@pending_docs'] = (int) trim(current($docs_pending_xpath)); + $max_time_xpath = $stats->xpath('//lst["stats"]/str[@name="autocommit maxTime"]'); + $max_time = (int) trim(current($max_time_xpath)); + // Convert to seconds. + $summary['@autocommit_time_seconds'] = $max_time / 1000; + $summary['@autocommit_time'] = format_interval($max_time / 1000); + $deletes_id_xpath = $stats->xpath('//lst["stats"]/long[@name="deletesById"]'); + $summary['@deletes_by_id'] = (int) trim(current($deletes_id_xpath)); + $deletes_query_xpath = $stats->xpath('//lst["stats"]/long[@name="deletesByQuery"]'); + $summary['@deletes_by_query'] = (int) trim(current($deletes_query_xpath)); + $summary['@deletes_total'] = $summary['@deletes_by_id'] + $summary['@deletes_by_query']; + $schema = $system_info->core->schema; + $summary['@schema_version'] = $schema; + $core = $stats->xpath('//lst["core"]/str[@name="coreName"]'); + $summary['@core_name'] = trim(current($core)); + $size_xpath = $stats->xpath('//lst["core"]/str[@name="indexSize"]'); + $summary['@index_size'] = trim(current($size_xpath)); + } + } + + return $summary; + } + + /** + * Implements SearchApiSolrConnectionInterface::clearCache(). + */ + public function clearCache() { + if ($cid = $this->getCacheId()) { + cache_clear_all($cid, 'cache_search_api_solr', TRUE); + cache_clear_all($cid, 'cache_search_api_solr', TRUE); + } + $this->luke = array(); + $this->stats = NULL; + $this->system_info = NULL; + } + + /** + * Checks the reponse code and throws an exception if it's not 200. + * + * @param object $response + * A response object. + * + * @return object + * The passed response object. + * + * @throws SearchApiException + * If the object's HTTP status is not 200. + */ + protected function checkResponse($response) { + $code = (int) $response->code; + + if ($code != 200) { + if ($code >= 400 && $code != 403 && $code != 404) { + // Add details, like Solr's exception message. + $response->status_message .= $response->data; + } + throw new SearchApiException('"' . $code . '" Status: ' . $response->status_message); + } + + return $response; + } + + /** + * Implements SearchApiSolrConnectionInterface::makeServletRequest(). + */ + public function makeServletRequest($servlet, array $params = array(), array $options = array()) { + // Add default params. + $params += array( + 'wt' => 'json', + 'json.nl' => self::NAMED_LIST_FORMAT, + ); + + $url = $this->constructUrl($servlet, $params); + $response = $this->makeHttpRequest($url, $options); + + return $this->checkResponse($response); + } + + /** + * Central method for making a GET operation against this Solr Server + */ + protected function sendRawGet($url, array $options = array()) { + $options['method'] = 'GET'; + $response = $this->makeHttpRequest($url, $options); + + return $this->checkResponse($response); + } + + /** + * Central method for making a POST operation against this Solr Server + */ + protected function sendRawPost($url, array $options = array()) { + $options['method'] = 'POST'; + // Normally we use POST to send XML documents. + if (empty($options['headers']['Content-Type'])) { + $options['headers']['Content-Type'] = 'text/xml; charset=UTF-8'; + } + $response = $this->makeHttpRequest($url, $options); + + return $this->checkResponse($response); + } + + /** + * Sends an HTTP request to Solr. + * + * This is just a wrapper around drupal_http_request(). + */ + protected function makeHttpRequest($url, array $options = array()) { + if (empty($options['method']) || $options['method'] == 'GET' || $options['method'] == 'HEAD') { + // Make sure we are not sending a request body. + $options['data'] = NULL; + } + if ($this->http_auth) { + $options['headers']['Authorization'] = $this->http_auth; + } + if ($this->stream_context) { + $options['context'] = $this->stream_context; + } + + $result = drupal_http_request($url, $options); + + if (!isset($result->code) || $result->code < 0) { + $result->code = 0; + $result->status_message = 'Request failed'; + $result->protocol = 'HTTP/1.0'; + } + // Additional information may be in the error property. + if (isset($result->error)) { + $result->status_message .= ': ' . check_plain($result->error); + } + + if (!isset($result->data)) { + $result->data = ''; + $result->response = NULL; + } + else { + $response = json_decode($result->data); + if (is_object($response)) { + foreach ($response as $key => $value) { + $result->$key = $value; + } + } + } + + return $result; + } + + /** + * Implements SearchApiSolrConnectionInterface::escape(). + */ + public static function escape($value, $version = 0) { + $replacements = array(); + + $specials = array('+', '-', '&&', '||', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', "\\"); + // Solr 4.x introduces regular expressions, making the slash also a special + // character. + if ($version >= 4) { + $specials[] = '/'; + } + + foreach ($specials as $special) { + $replacements[$special] = "\\$special"; + } + + return strtr($value, $replacements); + } + + /** + * Implements SearchApiSolrConnectionInterface::escapePhrase(). + */ + public static function escapePhrase($value) { + $replacements['"'] = '\"'; + $replacements["\\"] = "\\\\"; + return strtr($value, $replacements); + } + + /** + * Implements SearchApiSolrConnectionInterface::phrase(). + */ + public static function phrase($value) { + return '"' . self::escapePhrase($value) . '"'; + } + + /** + * Implements SearchApiSolrConnectionInterface::escapeFieldName(). + */ + public static function escapeFieldName($value) { + $value = str_replace(':', '\:', $value); + return $value; + } + + /** + * Returns the HTTP URL for a certain servlet on the Solr server. + * + * @param $servlet + * A string path to a Solr request handler. + * @param array $params + * Additional GET parameters to append to the URL. + * @param $added_query_string + * Additional query string to append to the URL. + * + * @return string + */ + protected function constructUrl($servlet, array $params = array(), $added_query_string = NULL) { + // PHP's built in http_build_query() doesn't give us the format Solr wants. + $query_string = $this->httpBuildQuery($params); + + if ($query_string) { + $query_string = '?' . $query_string; + if ($added_query_string) { + $query_string = $query_string . '&' . $added_query_string; + } + } + elseif ($added_query_string) { + $query_string = '?' . $added_query_string; + } + + return $this->base_url . $servlet . $query_string; + } + + /** + * Implements SearchApiSolrConnectionInterface::getBaseUrl(). + */ + public function getBaseUrl() { + return $this->base_url; + } + + /** + * Implements SearchApiSolrConnectionInterface::setBaseUrl(). + */ + public function setBaseUrl($url) { + $this->base_url = $url; + $this->update_url = NULL; + } + + /** + * Implements SearchApiSolrConnectionInterface::update(). + */ + public function update($rawPost, $timeout = FALSE) { + if (empty($this->update_url)) { + // Store the URL in an instance variable since many updates may be sent + // via a single instance of this class. + $this->update_url = $this->constructUrl(self::UPDATE_SERVLET, array('wt' => 'json')); + } + $options['data'] = $rawPost; + if ($timeout) { + $options['timeout'] = $timeout; + } + return $this->sendRawPost($this->update_url, $options); + } + + /** + * Implements SearchApiSolrConnectionInterface::addDocuments(). + */ + public function addDocuments(array $documents, $overwrite = NULL, $commitWithin = NULL) { + $attr = ''; + + if (isset($overwrite)) { + $attr .= ' overwrite="' . ($overwrite ? 'true"' : 'false"'); + } + if (isset($commitWithin)) { + $attr .= ' commitWithin="' . ((int) $commitWithin) . '"'; + } + + $rawPost = ""; + foreach ($documents as $document) { + if (is_object($document) && ($document instanceof SearchApiSolrDocument)) { + $rawPost .= $document->toXml(); + } + } + $rawPost .= ''; + + return $this->update($rawPost); + } + + /** + * Implements SearchApiSolrConnectionInterface::commit(). + */ + public function commit($waitSearcher = TRUE, $timeout = 3600) { + return $this->optimizeOrCommit('commit', $waitSearcher, $timeout); + } + + /** + * Implements SearchApiSolrConnectionInterface::deleteById(). + */ + public function deleteById($id, $timeout = 3600) { + return $this->deleteByMultipleIds(array($id), $timeout); + } + + /** + * Implements SearchApiSolrConnectionInterface::deleteByMultipleIds(). + */ + public function deleteByMultipleIds(array $ids, $timeout = 3600) { + $rawPost = ''; + + foreach ($ids as $id) { + $rawPost .= '' . htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8') . ''; + } + $rawPost .= ''; + + return $this->update($rawPost, $timeout); + } + + /** + * Implements SearchApiSolrConnectionInterface::deleteByQuery(). + */ + public function deleteByQuery($rawQuery, $timeout = 3600) { + $rawPost = '' . htmlspecialchars($rawQuery, ENT_NOQUOTES, 'UTF-8') . ''; + + return $this->update($rawPost, $timeout); + } + + /** + * Implements SearchApiSolrConnectionInterface::optimize(). + */ + public function optimize($waitSearcher = TRUE, $timeout = 3600) { + return $this->optimizeOrCommit('optimize', $waitSearcher, $timeout); + } + + /** + * Sends an commit or optimize command to the Solr server. + * + * Will be synchronous unless $waitSearcher is set to FALSE. + * + * @param string $type + * Either "commit" or "optimize". + * @param bool $waitSearcher + * (optional) Wait until a new searcher is opened and registered as the main + * query searcher, making the changes visible. Defaults to true. + * @param int $timeout + * Seconds to wait until timing out with an exception. Defaults to an hour. + * + * @return object + * A response object. + * + * @throws SearchApiException + * If an error occurs during the service call. + */ + protected function optimizeOrCommit($type, $waitSearcher = TRUE, $timeout = 3600) { + $waitSearcher = $waitSearcher ? '' : ' waitSearcher="false"'; + + if ($this->getSolrVersion() <= 3) { + $rawPost = "<$type$waitSearcher />"; + } + else { + $softCommit = ($this->soft_commit) ? ' softCommit="true"' : ''; + $rawPost = "<$type$waitSearcher$softCommit />"; + } + + $response = $this->update($rawPost, $timeout); + $this->clearCache(); + + return $response; + } + + /** + * Generates an URL-encoded query string. + * + * Works like PHP's built in http_build_query() (or drupal_http_build_query()) + * but uses rawurlencode() and no [] for repeated params, to be compatible + * with the Java-based servers Solr runs on. + * + * + * @param array $query + * The query parameters which should be set. + * @param string $parent + * Internal use only. + * + * @return string + * A query string to append (after "?") to a URL. + */ + protected function httpBuildQuery(array $query, $parent = '') { + $params = array(); + + foreach ($query as $key => $value) { + $key = ($parent ? $parent : rawurlencode($key)); + + // Recurse into children. + if (is_array($value)) { + $params[] = $this->httpBuildQuery($value, $key); + } + // If a query parameter value is NULL, only append its key. + elseif (!isset($value)) { + $params[] = $key; + } + else { + $params[] = $key . '=' . rawurlencode($value); + } + } + + return implode('&', $params); + } + + /** + * {@inheritdoc} + */ + public function search($query = NULL, array $params = array(), $method = 'GET') { + // Always use JSON. See + // http://code.google.com/p/solr-php-client/issues/detail?id=6#c1 for + // reasoning. + $params['wt'] = 'json'; + // Additional default params. + $params += array( + 'json.nl' => self::NAMED_LIST_FORMAT, + ); + if ($query) { + $params['q'] = $query; + } + // PHP's built-in http_build_query() doesn't give us the format Solr wants. + $queryString = $this->httpBuildQuery($params); + + if ($method == 'GET' || $method == 'AUTO') { + $searchUrl = $this->constructUrl(self::SEARCH_SERVLET, array(), $queryString); + if ($method == 'GET' || strlen($searchUrl) <= variable_get('search_api_solr_http_get_max_length', 4000)) { + return $this->sendRawGet($searchUrl); + } + } + + // Method is POST, or AUTO with a long query + $searchUrl = $this->constructUrl(self::SEARCH_SERVLET); + $options['data'] = $queryString; + $options['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + return $this->sendRawPost($searchUrl, $options); + } + +} diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.interface.inc b/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.interface.inc new file mode 100644 index 00000000..4a42db80 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/includes/solr_connection.interface.inc @@ -0,0 +1,365 @@ + 'Indexed', + 'T' => 'Tokenized', + 'S' => 'Stored', + 'M' => 'Multivalued', + 'V' => 'TermVector Stored', + 'o' => 'Store Offset With TermVector', + 'p' => 'Store Position With TermVector', + 'O' => 'Omit Norms', + 'L' => 'Lazy', + 'B' => 'Binary', + 'C' => 'Compressed', + 'f' => 'Sort Missing First', + 'l' => 'Sort Missing Last', + ); + + /** + * @var stdclass + * The original field object. + */ + protected $field; + + /** + * @var array + * An array of schema properties for this field. This will be a subset of + * the SearchApiSolrField::schemaLabels array. + */ + protected $schema; + + /** + * Constructs a field information object. + * + * @param stdClass $field + * A field object from Solr's "Luke" servlet. + */ + public function __construct($field) { + $this->field = $field; + } + + /** + * Gets the raw information of the field. + * + * @return object + * A field metadata object. + */ + public function getRaw() { + return $this->field; + } + + /** + * Gets the type of the Solr field, according to the Solr schema. + * + * Note that field types like "text", "boolean", and "date" are conventions, + * but their presence and behavior are entirely determined by the particular + * schema.xml file used by a Solr core. + * + * @return string + * The type of the Solr field. + */ + public function getType() { + return $this->field->type; + } + + /** + * Gets an array of field properties. + * + * @return array + * An array of properties describing the solr schema. The array keys are + * single-character codes, and the values are human-readable labels. This + * will be a subset of the SearchApiSolrField::schemaLabels array. + */ + public function getSchema() { + if (!isset($this->schema)) { + foreach (str_split(str_replace('-', '', $this->field->schema)) as $key) { + $this->schema[$key] = isset(self::$schemaLabels[$key]) ? self::$schemaLabels[$key] : $key; + } + } + return $this->schema; + } + + /** + * Gets the "dynamic base" of this field. + * + * This typically looks like 'ss_*, and is used to aggregate fields based on + * "hungarian" naming conventions. + * + * @return string + * The mask describing the solr aggregate field, if there is one. + */ + public function getDynamicBase() { + return isset($this->field->dynamicBase) ? $this->field->dynamicBase : NULL; + } + + /** + * Determines whether this field may be suitable for use as a key field. + * + * Unfortunately, it seems like the best way to find an actual uniqueKey field + * according to Solr is to examine the Solr core's schema.xml. + * + * @return bool + * Whether the field is suitable for use as a key. + */ + public function isPossibleKey() { + return !$this->getDynamicBase() + && !in_array($this->getType(), array('boolean', 'date', 'text')) + && $this->isStored() + && !$this->isMultivalued(); + } + + /** + * Determines whether a field is suitable for sorting. + * + * In order for a field to yield useful sorted results in Solr, it must be + * indexed, not multivalued, and not tokenized. It's ok if a field is + * tokenized and yields only one token, but there's no general way to check + * for that. + * + * @return bool + * Whether the field is suitable for sorting. + */ + public function isSortable() { + return $this->isIndexed() + && !$this->isMultivalued() + && !$this->isTokenized(); + } + + /** + * Determines whether this field is indexed. + * + * @return bool + * TRUE if the field is indexed, FALSE otherwise. + */ + public function isIndexed() { + $this->getSchema(); + return isset($this->schema['I']); + } + + /** + * Determines whether this field is tokenized. + * + * @return bool + * TRUE if the field is tokenized, FALSE otherwise. + */ + public function isTokenized() { + $this->getSchema(); + return isset($this->schema['T']); + } + + /** + * Determines whether this field is stored. + * + * @return bool + * TRUE if the field is stored, FALSE otherwise. + */ + public function isStored() { + $this->getSchema(); + return isset($this->schema['S']); + } + + /** + * Determines whether this field is multi-valued. + * + * @return bool + * TRUE if the field is multi-valued, FALSE otherwise. + */ + public function isMultivalued() { + $this->getSchema(); + return isset($this->schema['M']); + } + + /** + * Determines whether this field has stored term vectors. + * + * @return bool + * TRUE if the field has stored term vectors, FALSE otherwise. + */ + public function isTermVectorStored() { + $this->getSchema(); + return isset($this->schema['V']); + } + + /** + * Determines whether this field has the "termOffsets" option set. + * + * @return bool + * TRUE if the field has the "termOffsets" option set, FALSE otherwise. + */ + public function isStoreOffsetWithTermVector() { + $this->getSchema(); + return isset($this->schema['o']); + } + + /** + * Determines whether this field has the "termPositions" option set. + * + * @return bool + * TRUE if the field has the "termPositions" option set, FALSE otherwise. + */ + public function isStorePositionWithTermVector() { + $this->getSchema(); + return isset($this->schema['p']); + } + + /** + * Determines whether this field omits norms when indexing. + * + * @return bool + * TRUE if the field omits norms, FALSE otherwise. + */ + public function isOmitNorms() { + $this->getSchema(); + return isset($this->schema['O']); + } + + /** + * Determines whether this field is lazy-loaded. + * + * @return bool + * TRUE if the field is lazy-loaded, FALSE otherwise. + */ + public function isLazy() { + $this->getSchema(); + return isset($this->schema['L']); + } + + /** + * Determines whether this field is binary. + * + * @return bool + * TRUE if the field is binary, FALSE otherwise. + */ + public function isBinary() { + $this->getSchema(); + return isset($this->schema['B']); + } + + /** + * Determines whether this field is compressed. + * + * @return bool + * TRUE if the field is compressed, FALSE otherwise. + */ + public function isCompressed() { + $this->getSchema(); + return isset($this->schema['C']); + } + + /** + * Determines whether this field sorts missing entries first. + * + * @return bool + * TRUE if the field sorts missing entries first, FALSE otherwise. + */ + public function isSortMissingFirst() { + $this->getSchema(); + return isset($this->schema['f']); + } + + /** + * Determines whether this field sorts missing entries last. + * + * @return bool + * TRUE if the field sorts missing entries last, FALSE otherwise. + */ + public function isSortMissingLast() { + $this->getSchema(); + return isset($this->schema['l']); + } + +} diff --git a/sites/all/modules/contrib/search/search_api_solr/includes/spellcheck.inc b/sites/all/modules/contrib/search/search_api_solr/includes/spellcheck.inc new file mode 100644 index 00000000..58875b3f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/includes/spellcheck.inc @@ -0,0 +1,33 @@ +spellcheck->suggestions)) { + $suggestions = $response->spellcheck->suggestions; + foreach ($suggestions as $word => $data) { + foreach ($data->suggestion as $suggestion) { + $this->addSuggestion(new SearchApiSpellcheckSuggestion($word, $suggestion)); + } + } + } + } + +} diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.admin.inc b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.admin.inc new file mode 100644 index 00000000..bab6573a --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.admin.inc @@ -0,0 +1,62 @@ +' . t('List of configuration files found:') . ''; + + try { + // Retrieve the list of available files. + $files_list = search_api_solr_server_get_files($server); + + if (empty($files_list)) { + $form['info']['#markup'] = t('No files found.'); + return $form; + } + + $form['files'] = array( + '#type' => 'vertical_tabs', + ); + + // Generate a fieldset for each file. + foreach ($files_list as $file_name => $file_info) { + $file_date = format_date(strtotime($file_info['modified'])); + $escaped_file_name = check_plain($file_name); + + $form['files'][$file_name] = array( + '#title' => $escaped_file_name, + '#type' => 'fieldset', + ); + + $data = '

' . $escaped_file_name . '

'; + $data .= '

' . t('Last modified: @time.', array('@time' => $file_date)) . '

'; + + if ($file_info['size'] > 0) { + $file_data = $server->getFile($file_name); + $data .= '
' . check_plain($file_data->data) . '
'; + } + else { + $data .= '

' . t('The file is empty.') . '

'; + } + + $form['files'][$file_name]['data']['#markup'] = $data; + } + } + catch (SearchApiException $e) { + watchdog_exception('search_api_solr', $e, '%type while retrieving config files of Solr server @server: !message in %function (line %line of %file).', array('@server' => $server->name)); + $form['info']['#markup'] = t('An error occured while trying to load the list of files.'); + } + + return $form; +} diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.api.php b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.api.php new file mode 100644 index 00000000..99b93b3b --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.api.php @@ -0,0 +1,150 @@ +getOption('foobar')) { + $call_args['params']['foo'] = 'bar'; + } +} + +/** + * Change the way the index's field names are mapped to Solr field names. + * + * @param SearchApiIndex $index + * The index whose field mappings are altered. + * @param array $fields + * An associative array containing the index field names mapped to their Solr + * counterparts. The special fields 'search_api_id' and 'search_api_relevance' + * are also included. + */ +function hook_search_api_solr_field_mapping_alter(SearchApiIndex $index, array &$fields) { + if ($index->entity_type == 'node' && isset($fields['body:value'])) { + $fields['body:value'] = 'text'; + } +} + +/** + * Alter Solr documents before they are sent to Solr for indexing. + * + * @param array $documents + * An array of SearchApiSolrDocument objects ready to be indexed, generated + * from $items array. + * @param SearchApiIndex $index + * The search index for which items are being indexed. + * @param array $items + * An array of items being indexed. + */ +function hook_search_api_solr_documents_alter(array &$documents, SearchApiIndex $index, array $items) { + // Adds a "foo" field with value "bar" to all documents. + foreach ($documents as $document) { + $document->setField('foo', 'bar'); + } +} + +/** + * Lets modules alter the search results returned from a Solr search. + * + * @param array $results + * The results array that will be returned for the search. + * @param SearchApiQueryInterface $query + * The SearchApiQueryInterface object representing the executed search query. + * @param object $response + * The Solr response object. + */ +function hook_search_api_solr_search_results_alter(array &$results, SearchApiQueryInterface $query, $response) { + if (isset($response->facet_counts->facet_fields->custom_field)) { + // Do something with $results. + } +} + +/** + * Lets modules alter a Solr search request for a multi-index search. + * + * SearchApiSolrConnectionInterface::search() is called afterwards with these + * parameters. Please see this method for details on what should be altered + * where and what is set afterwards. + * + * @param array $call_args + * An associative array containing all three arguments to the + * SearchApiSolrConnectionInterface::search() call ("query", "params" and + * "method") as references. + * @param SearchApiMultiQueryInterface $query + * The object representing the executed search query. + */ +function hook_search_api_solr_multi_query_alter(array &$call_args, SearchApiMultiQueryInterface $query) { + if ($query->getOption('foobar')) { + $call_args['params']['foo'] = 'bar'; + } +} + +/** + * Provide Solr dynamic fields as Search API data types. + * + * This serves as a placeholder for documenting additional keys for + * hook_search_api_data_type_info() which are recognized by this module to + * automatically support dynamic field types from the schema. + * + * @return array + * In addition to the keys for the individual types that are defined by + * hook_search_api_data_type_info(), the following keys are regonized: + * - prefix: The Solr field name prefix to use for this type. Should match + * two existing dynamic fields definitions with names "{PREFIX}s_*" and + * "{PREFIX}m_*". + * - always multiValued: (optional) If TRUE, only the dynamic field name + * prefix (without the "_*" portion) with multiValued="true" should be given + * by "prefix", instead of the common prefix part for both the single-valued + * and the multi-valued field. This should be the case for all fulltext + * fields, since they might already be tokenized by the Search API. Defaults + * to FALSE. + * + *@see hook_search_api_data_type_info() + */ +function search_api_solr_hook_search_api_data_type_info() { + return array( + // You can use any identifier you want here, but it makes sense to use the + // field type name from schema.xml. + 'edge_n2_kw_text' => array( + // Stock hook_search_api_data_type_info() info: + 'name' => t('Fulltext (w/ partial matching)'), + 'fallback' => 'text', + // Dynamic field with name="te_*". + 'prefix' => 'te', + // Fulltext types should always be multi-valued. + 'always multiValued' => TRUE, + ), + 'tlong' => array( + // Stock hook_search_api_data_type_info() info: + 'name' => t('TrieLong'), + 'fallback' => 'integer', + // Dynamic fields with name="its_*" and name="itm_*". + 'prefix' => 'it', + ), + ); +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info new file mode 100644 index 00000000..80ce33a5 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.info @@ -0,0 +1,19 @@ +name = Solr search +description = Offers an implementation of the Search API that uses an Apache Solr server for indexing content. +dependencies[] = search_api +core = 7.x +package = Search + +files[] = includes/document.inc +files[] = includes/service.inc +files[] = includes/solr_connection.inc +files[] = includes/solr_connection.interface.inc +files[] = includes/solr_field.inc +files[] = includes/spellcheck.inc + +; Information added by Drupal.org packaging script on 2013-12-25 +version = "7.x-1.4" +core = "7.x" +project = "search_api_solr" +datestamp = "1387970905" + diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.install b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.install new file mode 100644 index 00000000..d67bb4d4 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.install @@ -0,0 +1,117 @@ + 'search_api_solr_service', 'enabled' => TRUE)); + $count = 0; + $unavailable = 0; + $last = NULL; + foreach ($servers as $server) { + if (!$server->ping()) { + ++$unavailable; + $last = $server; + } + ++$count; + } + if (!$count) { + return array(); + } + $ret['search_api_solr'] = array( + 'title' => t('Solr servers'), + 'value' => format_plural($count, '1 server', '@count servers'), + ); + if ($unavailable) { + if ($unavailable == 1) { + $ret['search_api_solr']['description'] = t('The Solr server of %name could not be reached.', + array('!url' => url('admin/config/search/search_api/server/' . $last->machine_name), '%name' => $last->name)); + } + else { + $ret['search_api_solr']['description'] = t('@count Solr servers could not be reached.', array('@count' => $unavailable)); + } + $ret['search_api_solr']['severity'] = REQUIREMENT_ERROR; + } + else { + $ret['search_api_solr']['description'] = format_plural($count, 'The Solr server could be reached.', 'All @count Solr servers could be reached.'); + $ret['search_api_solr']['severity'] = REQUIREMENT_OK; + } + } + + return $ret; +} + +/** + * Implements hook_uninstall(). + */ +function search_api_solr_uninstall() { + variable_del('search_api_solr_last_optimize'); + variable_del('search_api_solr_autocomplete_max_occurrences'); + variable_del('search_api_solr_index_prefix'); + variable_del('search_api_solr_http_get_max_length'); +} + +/** + * Implements hook_update_dependencies(). + */ +function search_api_solr_update_dependencies() { + // This update should run after primary IDs have been changed to machine names in the framework. + $dependencies['search_api_solr'][7101] = array( + 'search_api' => 7102, + ); + return $dependencies; +} + +/** + * Implements transition from using the index IDs to using machine names. + */ +function search_api_solr_update_7101() { + foreach (search_api_server_load_multiple(FALSE, array('class' => 'search_api_solr_service')) as $server) { + if ($server->enabled) { + $server->deleteItems('all'); + } + else { + $tasks = variable_get('search_api_tasks', array()); + $tasks[$server->machine_name][''] = array('clear all'); + variable_set('search_api_tasks', $tasks); + } + $query = db_select('search_api_index', 'i') + ->fields('i', array('machine_name')) + ->condition('server', $server->machine_name); + db_update('search_api_item') + ->fields(array( + 'changed' => REQUEST_TIME, + )) + ->condition('index_id', $query, 'IN') + ->execute(); + } + + return t('The Solr search module was updated. ' . + 'Please stop your Solr servers, replace their schema.xml with the new version and then start them again. ' . + 'All data indexed on Solr servers will have to be reindexed.'); +} + +/** + * Create the Search API Solr cache table {cache_search_api_solr}. + */ +function search_api_solr_update_7102() { + if (!db_table_exists('cache_search_api_solr')) { + $table = drupal_get_schema_unprocessed('system', 'cache'); + $table['description'] = 'Cache table for the Search API Solr module to store various data related to Solr servers.'; + db_create_table('cache_search_api_solr', $table); + } +} diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.module b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.module new file mode 100644 index 00000000..c98e4c3f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.module @@ -0,0 +1,317 @@ + 'Files', + 'description' => 'View Solr configuration files.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('search_api_solr_solr_config_form', 5), + 'access callback' => 'search_api_solr_access_server_files', + 'access arguments' => array(5), + 'file' => 'search_api_solr.admin.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => -1, + 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE, + ); + + return $items; +} + +/** + * Implements hook_search_api_service_info(). + */ +function search_api_solr_search_api_service_info() { + $variables = array( + '@solr_wiki_url' => url('http://wiki.apache.org/solr/SolrQuerySyntax'), + '@readme_url' => url(drupal_get_path('module', 'search_api_solr') . '/README.txt'), + ); + $services['search_api_solr_service'] = array( + 'name' => t('Solr service'), + 'description' => t('

Index items using an Apache Solr search server.

+
    +
  • See the Solr wiki for information about the "direct" parse mode.
  • +
  • Will use internal Solr preprocessors, so Search API preprocessors should for the most part be deactivated.
  • +
  • See the README.txt file provided with this module for details.
  • +
', $variables), + 'class' => 'SearchApiSolrService', + ); + return $services; +} + +/** + * Implements hook_help(). + */ +function search_api_solr_help($path, array $arg = array()) { + if ($path == 'admin/config/search/search_api') { + // Included because we need the REQUIREMENT_* constants. + include_once(DRUPAL_ROOT . '/includes/install.inc'); + module_load_include('install', 'search_api_solr'); + $reqs = search_api_solr_requirements('runtime'); + foreach ($reqs as $req) { + if (isset($req['description'])) { + $type = $req['severity'] == REQUIREMENT_ERROR ? 'error' : ($req['severity'] == REQUIREMENT_WARNING ? 'warning' : 'status'); + drupal_set_message($req['description'], $type); + } + } + } +} + +/** + * Implements hook_cron(). + * + * Used to execute an optimization operation on all enabled Solr servers once a + * day. + */ +function search_api_solr_cron() { + if (REQUEST_TIME - variable_get('search_api_solr_last_optimize', 0) > 86400) { + variable_set('search_api_solr_last_optimize', REQUEST_TIME); + $conditions = array('class' => 'search_api_solr_service', 'enabled' => TRUE); + foreach (search_api_server_load_multiple(FALSE, $conditions) as $server) { + try { + $server->getSolrConnection()->optimize(FALSE); + } + catch(Exception $e) { + watchdog_exception('search_api_solr', $e, '%type while optimizing Solr server @server: !message in %function (line %line of %file).', array('@server' => $server->name)); + } + } + } +} + +/** + * Implements hook_flush_caches(). + */ +function search_api_solr_flush_caches() { + return array('cache_search_api_solr'); +} + + +/** + * Implements hook_search_api_server_update(). + */ +function search_api_solr_search_api_server_update(SearchApiServer $server) { + if ($server->class === 'search_api_solr_service') { + $server->getSolrConnection()->clearCache(); + } +} + +/** + * Implements hook_views_api(). + */ +function search_api_solr_views_api() { + if (module_exists('search_api_views')) { + return array( + 'api' => 3, + ); + } +} + +/** + * Retrieves Solr-specific data for available data types. + * + * Returns the data type information for both the default Search API data types + * and custom data types defined by hook_search_api_data_type_info(). Names for + * default data types are not included, since they are not relevant to the Solr + * service class. + * + * We're adding some extra Solr field information for the default search api + * data types (as well as on behalf of a couple contrib field types). The + * extra information we're adding is documented in + * search_api_solr_hook_search_api_data_type_info(). You can use the same + * additional keys in hook_search_api_data_type_info() to support custom + * dynamic fields in your indexes with Solr. + * + * @param string|null $type + * (optional) A specific type for which the information should be returned. + * Defaults to returning all information. + * + * @return array|null + * If $type was given, information about that type or NULL if it is unknown. + * Otherwise, an array of all types. The format in both cases is the same as + * for search_api_get_data_type_info(). + * + * @see search_api_get_data_type_info() + * @see search_api_solr_hook_search_api_data_type_info() + */ +function search_api_solr_get_data_type_info($type = NULL) { + $types = &drupal_static(__FUNCTION__); + + if (!isset($types)) { + // Grab the stock search_api data types. + $types = search_api_get_data_type_info(); + + // Add our extras for the default search api fields. + $types += array( + 'text' => array( + 'prefix' => 'tm', + 'always multiValued' => TRUE, + ), + 'string' => array( + 'prefix' => 's', + ), + 'integer' => array( + 'prefix' => 'i', + ), + 'decimal' => array( + 'prefix' => 'f', + ), + 'date' => array( + 'prefix' => 'd', + ), + 'duration' => array( + 'prefix' => 'i', + ), + 'boolean' => array( + 'prefix' => 'b', + ), + 'uri' => array( + 'prefix' => 's', + ), + 'tokens' => array( + 'prefix' => 'tm', + 'always multiValued' => TRUE, + ), + ); + + // Extra data type info. + $extra_types_info = array( + 'location' => array( + 'prefix' => 'loc', + ), + 'geohash' => array( + 'prefix' => 'geo', + ), + ); + + // For the extra types, only add our extra info if it's already been defined. + foreach ($extra_types_info as $key => $info) { + if (array_key_exists($key, $types)) { + // Merge our extras into the data type info + $types[$key] += $info; + } + } + } + + // Return the info. + if (isset($type)) { + return isset($types[$type]) ? $types[$type] : NULL; + } + return $types; +} + +/** + * Retrieves a list of all config files of a server. + * + * @param SearchApiServer $server + * The Solr server whose files should be retrieved. + * @param string $dir_name + * (optional) The directory that should be searched for files. Defaults to the + * root config directory. + * + * @return array + * An associative array of all config files in the given directory. The keys + * are the file names, values are arrays with information about the file. The + * files are returned in alphabetical order and breadth-first. + * + * @throws SearchApiException + * If a problem occurred while retrieving the files. + */ +function search_api_solr_server_get_files(SearchApiServer $server, $dir_name = NULL) { + $response = $server->getFile($dir_name); + + // Search for directories and recursively merge directory files. + $files_data = json_decode($response->data, TRUE); + $files_list = $files_data['files']; + $result = array('' => array()); + + foreach ($files_list as $file_name => $file_info) { + if (empty($file_info['directory'])) { + $result[''][$file_name] = $file_info; + } + else { + $result[$file_name] = search_api_solr_server_get_files($server, $file_name); + } + } + + ksort($result); + ksort($result['']); + return array_reduce($result, 'array_merge', array()); +} + +/** + * @deprecated + * + * @see search_api_solr_access_server_files() + */ +function search_api_access_server_files(SearchApiServer $server) { + return search_api_solr_access_server_files($server); +} + +/** + * Access callback for a server's "Files" tab. + * + * Grants access if the user has the "administer search_api" permission and the + * server is a Solr server. + * + * @param SearchApiServer $server + * The server for which access should be tested. + * + * @return bool + * TRUE if access should be granted, FALSE otherwise. + */ +function search_api_solr_access_server_files(SearchApiServer $server) { + if (!user_access('administer search_api')) { + return FALSE; + } + $service_info = search_api_get_service_info($server->class); + $service_class = $service_info['class']; + + if (empty($service_class) || !class_exists($service_class)) { + // Service class not found. + return FALSE; + } + if ($service_class == 'SearchApiSolrService' || in_array('SearchApiSolrService', class_parents($service_class))) { + // It's an SearchApiSolrService based connection class. + return TRUE; + } + + return FALSE; +} + +/** + * Switches a server to use clean identifiers. + * + * Used as a submit callback in SearchApiSolrService::configurationForm(). + */ +function _search_api_solr_switch_to_clean_ids(array $form, array &$form_state) { + $server = $form_state['server']; + $server->options['clean_ids'] = TRUE; + $server->save(); + drupal_set_message(t('The Solr server was successfully switched to use clean field identifiers.')); + + $count = 0; + $conditions['server'] = $server->machine_name; + $conditions['enabled'] = 1; + foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) { + if (!empty($index->options['fields'])) { + foreach ($index->options['fields'] as $key => $field) { + if (strpos($key, ':') !== FALSE) { + $index->reindex(); + ++$count; + break; + } + } + } + } + if ($count) { + $msg = format_plural($count, '1 index was scheduled for re-indexing.', '@count indexes were scheduled for re-indexing.'); + drupal_set_message($msg); + } +} diff --git a/sites/all/modules/contrib/search/search_api_solr/search_api_solr.views.inc b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.views.inc new file mode 100644 index 00000000..657b7ace --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/search_api_solr.views.inc @@ -0,0 +1,62 @@ +server(); + if (!$server || empty($server->options['retrieve_data'])) { + return; + } + // Fill in base data. + $key = 'search_api_index_' . $index->machine_name; + $table = & $data[$key]; + + try { + $wrapper = $index->entityWrapper(NULL, FALSE); + } + catch (EntityMetadataWrapperException $e) { + watchdog_exception('search_api_solr', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); + continue; + } + + // Remember fields that aren't added by data alterations, etc. (since + // there isn't any other way to tell them apart). + $normal_fields = array(); + foreach ($wrapper as $key => $property) { + $normal_fields[$key] = TRUE; + } + + try { + $wrapper = $index->entityWrapper(NULL); + } + catch (EntityMetadataWrapperException $e) { + watchdog_exception('search_api_solr', $e, "%type while retrieving metadata for index %index: !message in %function (line %line of %file).", array('%index' => $index->name), WATCHDOG_WARNING); + continue; + } + + // Add field handlers for items added by data alterations, etc. + foreach ($wrapper as $key => $property) { + if (empty($normal_fields[$key])) { + $info = $property->info(); + if ($info) { + entity_views_field_definition($key, $info, $table); + } + } + } + } + } + catch (Exception $e) { + watchdog_exception('search_api_views', $e); + } +} diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/elevate.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/elevate.xml new file mode 100644 index 00000000..71ea0006 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/elevate.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/mapping-ISOLatin1Accent.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/mapping-ISOLatin1Accent.txt new file mode 100644 index 00000000..b92d03c5 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/mapping-ISOLatin1Accent.txt @@ -0,0 +1,14 @@ +# This file contains character mappings for the default fulltext field type. +# The source characters (on the left) will be replaced by the respective target +# characters before any other processing takes place. +# Lines starting with a pound character # are ignored. +# +# For sensible defaults, use the mapping-ISOLatin1Accent.txt file distributed +# with the example application of your Solr version. +# +# Examples: +# "À" => "A" +# "\u00c4" => "A" +# "\u00c4" => "\u0041" +# "æ" => "ae" +# "\n" => " " diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/protwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/protwords.txt new file mode 100644 index 00000000..cda85814 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/protwords.txt @@ -0,0 +1,7 @@ +#----------------------------------------------------------------------- +# This file blocks words from being operated on by the stemmer and word delimiter. +& +< +> +' +" diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema.xml new file mode 100644 index 00000000..f8b7c9dc --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema.xml @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + content + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema_extra_fields.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema_extra_fields.xml new file mode 100644 index 00000000..9ecd5f4f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema_extra_fields.xml @@ -0,0 +1,23 @@ + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema_extra_types.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema_extra_types.xml new file mode 100644 index 00000000..e82072e2 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/schema_extra_types.xml @@ -0,0 +1,30 @@ + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrconfig.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrconfig.xml new file mode 100644 index 00000000..5798444a --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrconfig.xml @@ -0,0 +1,1606 @@ + + + + + + + + + ${solr.abortOnConfigurationError:true} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + 4 + + 32 + + + + 2147483647 + 100000 + 1000 + + + + + + + + + single + + + + + + + + + false + 32 + 10 + + + false + + + true + + + + + 1 + + 0 + + + + + + false + + + + + + + + + + + + + + + + ${solr.autoCommit.MaxDocs:10000} + ${solr.autoCommit.MaxTime:120000} + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 20 + + + 200 + + + + + + + + + + + + solr rocks010 + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dismax + explicit + true + 0.01 + + ${solr.pinkPony.timeAllowed:-1} + *:* + + + false + + true + false + + 1 + + + spellcheck + + + + + + + 1 + 1 + 3 + 15 + 20 + false + + ${solr.mlt.timeAllowed:2000} + + + + + + + explicit + true + + + + + + + + + + + + + + + + + + + + + + text + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + + + pinkPony + solrpingquery + + + all + + + + + + + explicit + true + + + + + + + ${solr.replication.master:false} + commit + startup + ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} + + + ${solr.replication.slave:false} + ${solr.replication.masterUrl:http://localhost:8983/solr}/replication + ${solr.replication.pollInterval:00:00:60} + + + + + + + + + false + false + 1 + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + + + + + + 5 + + + + + + + + + + + + + *:* + + + + + + + + + + + + + + textSpell + + + + default + spell + spellchecker + true + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrconfig_extra.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrconfig_extra.xml new file mode 100644 index 00000000..c5bc3acf --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrconfig_extra.xml @@ -0,0 +1,80 @@ + + + +textSpell + + + + + + default + spell + spellchecker + true + + + + + + + + + + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrcore.properties b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrcore.properties new file mode 100644 index 00000000..f5ede05e --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/solrcore.properties @@ -0,0 +1,10 @@ +# Defines Solr properties for this specific core. +solr.replication.master=false +solr.replication.slave=false +solr.replication.pollInterval=00:00:60 +solr.replication.masterUrl=http://localhost:8983/solr +solr.replication.confFiles=schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml +solr.mlt.timeAllowed=2000 +solr.pinkPony.timeAllowed=-1 +solr.autoCommit.MaxDocs=10000 +solr.autoCommit.MaxTime=120000 diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/stopwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/stopwords.txt new file mode 100644 index 00000000..045f6d10 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/stopwords.txt @@ -0,0 +1,4 @@ +# Contains words which shouldn't be indexed for fulltext fields, e.g., because +# they're to common. For documentation of the format, see +# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.StopFilterFactory +# (Lines starting with a pound character # are ignored.) diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/synonyms.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/synonyms.txt new file mode 100644 index 00000000..7d22eea6 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/1.4/synonyms.txt @@ -0,0 +1,3 @@ +# Contains synonyms to use for your index. For the format used, see +# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.SynonymFilterFactory +# (Lines starting with a pound character # are ignored.) diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/elevate.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/elevate.xml new file mode 100644 index 00000000..71ea0006 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/elevate.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/mapping-ISOLatin1Accent.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/mapping-ISOLatin1Accent.txt new file mode 100644 index 00000000..b92d03c5 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/mapping-ISOLatin1Accent.txt @@ -0,0 +1,14 @@ +# This file contains character mappings for the default fulltext field type. +# The source characters (on the left) will be replaced by the respective target +# characters before any other processing takes place. +# Lines starting with a pound character # are ignored. +# +# For sensible defaults, use the mapping-ISOLatin1Accent.txt file distributed +# with the example application of your Solr version. +# +# Examples: +# "À" => "A" +# "\u00c4" => "A" +# "\u00c4" => "\u0041" +# "æ" => "ae" +# "\n" => " " diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/protwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/protwords.txt new file mode 100644 index 00000000..cda85814 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/protwords.txt @@ -0,0 +1,7 @@ +#----------------------------------------------------------------------- +# This file blocks words from being operated on by the stemmer and word delimiter. +& +< +> +' +" diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema.xml new file mode 100644 index 00000000..acc5e4b7 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema.xml @@ -0,0 +1,546 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + content + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema_extra_fields.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema_extra_fields.xml new file mode 100644 index 00000000..9ecd5f4f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema_extra_fields.xml @@ -0,0 +1,23 @@ + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema_extra_types.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema_extra_types.xml new file mode 100644 index 00000000..e82072e2 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/schema_extra_types.xml @@ -0,0 +1,30 @@ + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrconfig.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrconfig.xml new file mode 100644 index 00000000..7091ba48 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrconfig.xml @@ -0,0 +1,1613 @@ + + + + + + + + + ${solr.abortOnConfigurationError:true} + + + ${solr.luceneMatchVersion:LUCENE_35} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + 4 + + 32 + + + + 2147483647 + 100000 + 1000 + + + + + + + + + single + + + + + + + + + false + 32 + 10 + + + false + + + true + + + + + 1 + + 0 + + + + + + false + + + + + + + + + + + + + + + + ${solr.autoCommit.MaxDocs:10000} + ${solr.autoCommit.MaxTime:120000} + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 20 + + + 200 + + + + + + + + + + + + solr rocks010 + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + edismax + explicit + true + 0.01 + + ${solr.pinkPony.timeAllowed:-1} + *:* + + + false + + true + false + + 1 + + + spellcheck + + + + + + + 1 + 1 + 3 + 15 + 20 + false + + ${solr.mlt.timeAllowed:2000} + + + + + + + explicit + true + + + + + + + + + + + + + + + + + + + + + + text + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + + + pinkPony + solrpingquery + + + all + + + + + + + explicit + true + + + + + + + ${solr.replication.master:false} + commit + startup + ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} + + + ${solr.replication.slave:false} + ${solr.replication.masterUrl:http://localhost:8983/solr}/replication + ${solr.replication.pollInterval:00:00:60} + + + + + + + + + false + false + 1 + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + + + + + + 5 + + + + + + + + + + + + + *:* + + + + + + + + + + + + textSpell + + + + default + spell + spellchecker + true + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrconfig_extra.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrconfig_extra.xml new file mode 100644 index 00000000..c5bc3acf --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrconfig_extra.xml @@ -0,0 +1,80 @@ + + + +textSpell + + + + + + default + spell + spellchecker + true + + + + + + + + + + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrcore.properties b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrcore.properties new file mode 100644 index 00000000..bc468862 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/solrcore.properties @@ -0,0 +1,16 @@ +# Defines Solr properties for this specific core. +solr.replication.master=false +solr.replication.slave=false +solr.replication.pollInterval=00:00:60 +solr.replication.masterUrl=http://localhost:8983/solr +solr.replication.confFiles=schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml +solr.mlt.timeAllowed=2000 +# You should not set your luceneMatchVersion to anything lower than your Solr +# Version. +solr.luceneMatchVersion=LUCENE_35 +solr.pinkPony.timeAllowed=-1 +# autoCommit after 10000 docs +solr.autoCommit.MaxDocs=10000 +# autoCommit after 2 minutes +solr.autoCommit.MaxTime=120000 +solr.contrib.dir=../../contrib diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/stopwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/stopwords.txt new file mode 100644 index 00000000..d7f243e4 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/stopwords.txt @@ -0,0 +1,4 @@ +# Contains words which shouldn't be indexed for fulltext fields, e.g., because +# they're too common. For documentation of the format, see +# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.StopFilterFactory +# (Lines starting with a pound character # are ignored.) diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/synonyms.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/synonyms.txt new file mode 100644 index 00000000..7d22eea6 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/3.x/synonyms.txt @@ -0,0 +1,3 @@ +# Contains synonyms to use for your index. For the format used, see +# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.SynonymFilterFactory +# (Lines starting with a pound character # are ignored.) diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/elevate.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/elevate.xml new file mode 100644 index 00000000..71ea0006 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/elevate.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/mapping-ISOLatin1Accent.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/mapping-ISOLatin1Accent.txt new file mode 100644 index 00000000..b92d03c5 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/mapping-ISOLatin1Accent.txt @@ -0,0 +1,14 @@ +# This file contains character mappings for the default fulltext field type. +# The source characters (on the left) will be replaced by the respective target +# characters before any other processing takes place. +# Lines starting with a pound character # are ignored. +# +# For sensible defaults, use the mapping-ISOLatin1Accent.txt file distributed +# with the example application of your Solr version. +# +# Examples: +# "À" => "A" +# "\u00c4" => "A" +# "\u00c4" => "\u0041" +# "æ" => "ae" +# "\n" => " " diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/protwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/protwords.txt new file mode 100644 index 00000000..cda85814 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/protwords.txt @@ -0,0 +1,7 @@ +#----------------------------------------------------------------------- +# This file blocks words from being operated on by the stemmer and word delimiter. +& +< +> +' +" diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema.xml new file mode 100644 index 00000000..6e2b615b --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema.xml @@ -0,0 +1,552 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + content + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema_extra_fields.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema_extra_fields.xml new file mode 100644 index 00000000..9ecd5f4f --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema_extra_fields.xml @@ -0,0 +1,23 @@ + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema_extra_types.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema_extra_types.xml new file mode 100644 index 00000000..e82072e2 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/schema_extra_types.xml @@ -0,0 +1,30 @@ + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrconfig.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrconfig.xml new file mode 100644 index 00000000..cc5f5225 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrconfig.xml @@ -0,0 +1,1633 @@ + + + + + + + + + ${solr.abortOnConfigurationError:true} + + + ${solr.luceneMatchVersion:LUCENE_40} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + 4 + + 32 + + + + 2147483647 + 100000 + 1000 + + + + + + + + + single + + + + + + false + + + true + + + + + 1 + + 0 + + + + + + false + + + + + + + + + + + + + + + + ${solr.autoCommit.MaxDocs:10000} + ${solr.autoCommit.MaxTime:120000} + + + + + ${solr.autoSoftCommit.MaxDocs:2000} + ${solr.autoSoftCommit.MaxTime:10000} + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 20 + + + 200 + + + + + + + + + + + + solr rocks010 + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + edismax + explicit + true + 0.01 + + ${solr.pinkPony.timeAllowed:-1} + *:* + + + false + + true + false + + 1 + + + spellcheck + + + + + + + 1 + 1 + 3 + 15 + 20 + false + + ${solr.mlt.timeAllowed:2000} + + + + + + + explicit + true + + + + + + + + + + + + + + + + + + + + + + text + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + + + pinkPony + solrpingquery + + + all + + + + + + + explicit + true + + + + + + + ${solr.replication.master:false} + commit + startup + ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml} + + + ${solr.replication.slave:false} + ${solr.replication.masterUrl:http://localhost:8983/solr}/replication + ${solr.replication.pollInterval:00:00:60} + + + + + + + true + json + true + + + + + + + + + false + false + 1 + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + + + + + + 5 + + + + + + + + + + + + + *:* + + + + + + + + + + + + textSpell + + + + default + spell + spellchecker + true + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrconfig_extra.xml b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrconfig_extra.xml new file mode 100644 index 00000000..c5bc3acf --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrconfig_extra.xml @@ -0,0 +1,80 @@ + + + +textSpell + + + + + + default + spell + spellchecker + true + + + + + + + + + + + + + + + diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrcore.properties b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrcore.properties new file mode 100644 index 00000000..b7f8f6c8 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/solrcore.properties @@ -0,0 +1,20 @@ +# Defines Solr properties for this specific core. +solr.replication.master=false +solr.replication.slave=false +solr.replication.pollInterval=00:00:60 +solr.replication.masterUrl=http://localhost:8983/solr +solr.replication.confFiles=schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml +solr.mlt.timeAllowed=2000 +# You should not set your luceneMatchVersion to anything lower than your Solr +# Version. +solr.luceneMatchVersion=LUCENE_40 +solr.pinkPony.timeAllowed=-1 +# autoCommit after 10000 docs +solr.autoCommit.MaxDocs=10000 +# autoCommit after 2 minutes +solr.autoCommit.MaxTime=120000 +# autoSoftCommit after 2000 docs +solr.autoSoftCommit.MaxDocs=2000 +# autoSoftCommit after 10 seconds +solr.autoSoftCommit.MaxTime=10000 +solr.contrib.dir=../../../contrib diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/stopwords.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/stopwords.txt new file mode 100644 index 00000000..d7f243e4 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/stopwords.txt @@ -0,0 +1,4 @@ +# Contains words which shouldn't be indexed for fulltext fields, e.g., because +# they're too common. For documentation of the format, see +# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.StopFilterFactory +# (Lines starting with a pound character # are ignored.) diff --git a/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/synonyms.txt b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/synonyms.txt new file mode 100644 index 00000000..7d22eea6 --- /dev/null +++ b/sites/all/modules/contrib/search/search_api_solr/solr-conf/4.x/synonyms.txt @@ -0,0 +1,3 @@ +# Contains synonyms to use for your index. For the format used, see +# http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.SynonymFilterFactory +# (Lines starting with a pound character # are ignored.)