diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 0c123388..f39c6961 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,330 +1,349 @@
-Search API Solr Search 1.x, dev (xxxx-xx-xx):
----------------------------------------------
-- #3118390 by drunken monkey, bburg: Fixed
- SearchApiSolrService::queryMultiple().
-
-Search API Solr Search 1.15 (2019-11-07):
------------------------------------------
-- #3082039 by xlin, drunken monkey: Added a hidden variable for switching the
- Solr connection class.
-- #3074587 by michel.settembrino, drunken monkey: Fixed excerpt generation in
- some edge cases.
-- #3059045 by tomdearden, drunken monkey: Fixed problems with multi-valued date
- fields at search time.
-- #3048646 by drunken monkey: Fixed edge case problem when logging Solr
- responses.
-- #2128537 by Frando, drunken monkey, Ronino, g089h515r806, jmdeleon: Added
- support for facet queries.
-- #3012216 by drunken monkey: Fixed excerpts for multi-index search views.
-- #3013932 by drunken monkey, RajeevChoudhary: Fixed error with unset query
- fields.
-- #3006424 by drunken monkey: Fixed problem when retrieving multi-valued date
- fields.
-
-Search API Solr Search 1.14 (2018-10-10):
------------------------------------------
-- Security issue: Fixed possible information disclosure in generated excerpts.
-- #3003819 by mandclu, drunken monkey: Added support for indexing nested
- documents.
-
-Search API Solr Search 1.13 (2018-09-17):
------------------------------------------
-- #2998931 by drunken monkey, mahipal46: Fixed warning during excerpt creation.
-- #2459457 by das-peter, drunken monkey, ruloweb, SpadXIII, mkalkbrenner: Added
- support for the "(not) between" operator.
-- #2970829 by donquixote, drunken monkey: Fixed "missing" facet in PHP 7.
-- #2916951 by drunken monkey, mkalkbrenner, osopolar, jcnventura, arcadLemon,
- Siridivi: Added support for Solr 7.x.
-- #2949293 by Novitsh: Fixed path to README.txt on multilingual sites.
-- #2900308 by cgoffin, drunken monkey: Fixed retrieval of multi-valued date
- fields in results.
-- #2824956 by lex0r, drunken monkey: Added support for atomic updates on API
- level.
-- #2867076 by Sardis: Fixed Solr config name for Solr 5.x.
-
-Search API Solr Search 1.12 (2017-02-23):
------------------------------------------
-- #2612770 by Temoor, drunken monkey: Fixed conjunction in complex filter
- conditions.
-- #2711685 by berliner: Added support for Solr 6.
-- #2842661 by gabrielu, drunken monkey: Fixed duplicate code line in
- clearCache().
-- #2834159 by zniki.ru: Fixed left-over explicit
- SearchApiSolrConnection::escapeFieldName() call.
-- #2677912 by jts86, mian3010: Added option to disable committing for Solr.
-- #2828473 by detroz: Fixed incorrect variable initialization.
-- #2826565 by Sardis, drunken monkey: Fixed multi-valued field highlighting.
-- #2807327 by drunken monkey: Fixed configuration form descriptions.
-- #2772199 by drunken monkey: Added a warning to the description for the
- highlighting server option.
-- #2733625 by joelstein, drunken monkey: Fixed notice in
- Connection::getFields().
-
-Search API Solr Search 1.11 (2016-07-06):
------------------------------------------
-- #2710397 by drunken monkey: Fixed escaping of highlighting tags.
-- #2636016 by drunken monkey: Fixed location search distance facets.
-
-Search API Solr Search 1.10 (2016-03-14):
------------------------------------------
-- #2604322 by lex0r, drunken monkey: Added the option to log all Solr queries.
-- #2645366 by tedfordgif, drunken monkey: Fixed browser incorrectly filling the
- HTTP Auth form fields.
-- #2611716 by aditya_anurag, drunken monkey: Improved the method documentation
- comments.
-- #2599658 by Loparev, drunken monkey: Added the possibility to highlight
- non-fulltext fields.
-- #2598288 by ethan.han777, JeroenT, drunken monkey: Fixed potential notices in
- the spellchecker code.
-- #2564927 by thePanz: Added support for random sorting in grouped searches.
-- #2527528 by maximpodorov, drunken monkey: Fixed searching of string fields
- with leading/trailing spaces.
-- #2513314 by fortis, drunken monkey: Fixed error for empty filters.
-- #2551763 by drunken monkey: Fixed mention of INSTALL.txt in an error message.
-
-Search API Solr Search 1.9 (2015-08-30):
-----------------------------------------
-- #2503829 by das-peter: Added support for grouping on "magic" fields.
-- #2503617 by tobiasb: Fixed undefined "status_message" property in HTTP
- response object.
-- #2313591 by thePanz, nadavoid: Added support for random sorting.
-- #2004596 by drunken monkey: Fixed workarounds for MLT problems.
-- #2486533 by drunken monkey: Fixed the default operator in the Solr 5 configs.
-- #2466897 by drunken monkey: Fixed Solr version options in server settings.
-- #1918904 by arnested, ramlev, drunken monkey: Added an alter hook for
- autocomplete suggestions.
-- #2532812 by drunken monkey: Improved performance for filter-only queries.
-- #2463523 by bmunslow: Fixed field boosts in multi-index searches.
-
-Search API Solr Search 1.8 (2015-06-15):
-----------------------------------------
-- #2502511 by drunken monkey: Fixed index removal on Solr 5.
-- #2441117 by drunken monkey: Fixed unnecessary reindexing after changing
- fields' boosts.
-
-Search API Solr Search 1.7 (2015-06-08):
-----------------------------------------
-- #2466489 by drunken monkey: Changed installation instructions to point to the
- handbook.
-- #2486533 by drunken monkey: Fixed default operator in Solr 5.
-- #2456159 by drunken monkey: Updated config files to their latest version from
- the common configs.
-- #2442077 by drunken monkey, basvredeling: Added support for Solr 5.
-- #2451037 by drunken monkey: Fixed timeout errors during indexing.
-- #2054551 by paolomainardi, drunken monkey, lex0r: Added possibility to
- retrieve Solr field values in multi-index searches.
-- #1908990 by drunken monkey: Fixed various issues with excerpts.
-- #2368399 by das-peter, drunken monkey: Added Support for location filtering
- with a bounding box.
-- #2333133 by drunken monkey: Fixed behavior of filters on fulltext fields.
-
-Search API Solr Search 1.6 (2014-09-08):
-----------------------------------------
-- #2050961 by das-peter, drunken monkey: Added proximity/distance information
- to search results.
-- #2242073 by RaF: Fixed handling of custom negative filters in filter-only
- searches.
-- #2290601 by drunken monkey: Fixed handling of complex keywords and OR facets.
-- #2083357 by drunken monkey: Added note that Solr 4.x server paths should be
- specified with core.
-- #2270767 by RaF: Fixed search_api_solr_views_data_alter() not always
- returning all virtual fields.
-
-Search API Solr Search 1.5 (2014-05-23):
-----------------------------------------
-- #2216895 by das-peter: Added support for empty/non-empty conditions on
- location field types.
-- #2162627 by drunken monkey: Removed Solr 1.4 support.
-- #2175829 by danquah, drunken monkey: Fixed error when admin interface is not
- accessible.
-- #2222037 by drunken monkey: Fixed "Files" tab in Solr 4.7.
-- #2151719 by Derimagia, drunken monkey: Added an alter hook for multi-index
- search results.
-- #1776534 by drunken monkey, e2thex: Added support for using a Solr server
- with multiple sites.
-- #2152337 by drunken monkey: Removed confusing "multiple text fields" section
- from README.txt.
-- #2099559 by drunken monkey: Made optimizing the Solr server optional.
-- #2146749 by drunken monkey: Added soft commits as the default for Solr 4.
-- #1773440 by drunken monkey: Added performance improvement for “filter
- only” queries.
-- #2147573 by drunken monkey: Improved error handling.
-
-Search API Solr Search 1.4 (2013-12-25):
-----------------------------------------
-- #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 (2013-10-23):
-----------------------------------------
-- #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 (2013-09-01):
-----------------------------------------
-- #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 (2013-07-21):
-----------------------------------------
-- #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 (2013-06-09):
-----------------------------------------
-- #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 (2013-05-17):
-----------------------------------------------
-- #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 (2013-04-22):
-----------------------------------------------
-- #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 (2013-01-06):
-----------------------------------------------
-- #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 (2012-05-23):
-----------------------------------------------
-- 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 (2011-11-10):
-----------------------------------------------
-- #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 (2011-09-08):
-------------------------------------------------
-- #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 (2011-06-06):
-------------------------------------------------
-- #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 (2011-03-04):
-------------------------------------------------
-- #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 (2010-11-29):
-------------------------------------------------
-Basic functionality is in place and quite well-tested, including support for
-facets and for multi-index searches.
+Solr search 1.x, xxxx-xx-xx (development version)
+-----------------------
+
+Solr search 1.19, 2025-08-20
+-----------------------
+- Improved Solr 8 & 9 default configurations
+
+Solr search 1.18, 2025-05-13
+-----------------------
+- Add Solr 9 default configuration
+
+Search API Solr Search 1.17 (2025-04-07):
+-----------------------------------------
+- #3254212 by johnrosswvsu, drunken monkey: Added support for Solr 8 and fixes
+ for newer PHP versions.
+
+Search API Solr Search 1.16 (2023-11-02):
+-----------------------------------------
+- #3324942 by Ronino, drunken monkey: Fixed deprecation warning in
+ SearchApiSolrDocument::getIterator().
+- #1845920 by silverham, drunken monkey: Added config option for highlight tags.
+- #3118390 by drunken monkey, bburg: Fixed
+ SearchApiSolrService::queryMultiple().
+
+Search API Solr Search 1.15 (2019-11-07):
+-----------------------------------------
+- #3082039 by xlin, drunken monkey: Added a hidden variable for switching the
+ Solr connection class.
+- #3074587 by michel.settembrino, drunken monkey: Fixed excerpt generation in
+ some edge cases.
+- #3059045 by tomdearden, drunken monkey: Fixed problems with multi-valued date
+ fields at search time.
+- #3048646 by drunken monkey: Fixed edge case problem when logging Solr
+ responses.
+- #2128537 by Frando, drunken monkey, Ronino, g089h515r806, jmdeleon: Added
+ support for facet queries.
+- #3012216 by drunken monkey: Fixed excerpts for multi-index search views.
+- #3013932 by drunken monkey, RajeevChoudhary: Fixed error with unset query
+ fields.
+- #3006424 by drunken monkey: Fixed problem when retrieving multi-valued date
+ fields.
+
+Search API Solr Search 1.14 (2018-10-10):
+-----------------------------------------
+- Security issue: Fixed possible information disclosure in generated excerpts.
+- #3003819 by mandclu, drunken monkey: Added support for indexing nested
+ documents.
+
+Search API Solr Search 1.13 (2018-09-17):
+-----------------------------------------
+- #2998931 by drunken monkey, mahipal46: Fixed warning during excerpt creation.
+- #2459457 by das-peter, drunken monkey, ruloweb, SpadXIII, mkalkbrenner: Added
+ support for the "(not) between" operator.
+- #2970829 by donquixote, drunken monkey: Fixed "missing" facet in PHP 7.
+- #2916951 by drunken monkey, mkalkbrenner, osopolar, jcnventura, arcadLemon,
+ Siridivi: Added support for Solr 7.x.
+- #2949293 by Novitsh: Fixed path to README.txt on multilingual sites.
+- #2900308 by cgoffin, drunken monkey: Fixed retrieval of multi-valued date
+ fields in results.
+- #2824956 by lex0r, drunken monkey: Added support for atomic updates on API
+ level.
+- #2867076 by Sardis: Fixed Solr config name for Solr 5.x.
+
+Search API Solr Search 1.12 (2017-02-23):
+-----------------------------------------
+- #2612770 by Temoor, drunken monkey: Fixed conjunction in complex filter
+ conditions.
+- #2711685 by berliner: Added support for Solr 6.
+- #2842661 by gabrielu, drunken monkey: Fixed duplicate code line in
+ clearCache().
+- #2834159 by zniki.ru: Fixed left-over explicit
+ SearchApiSolrConnection::escapeFieldName() call.
+- #2677912 by jts86, mian3010: Added option to disable committing for Solr.
+- #2828473 by detroz: Fixed incorrect variable initialization.
+- #2826565 by Sardis, drunken monkey: Fixed multi-valued field highlighting.
+- #2807327 by drunken monkey: Fixed configuration form descriptions.
+- #2772199 by drunken monkey: Added a warning to the description for the
+ highlighting server option.
+- #2733625 by joelstein, drunken monkey: Fixed notice in
+ Connection::getFields().
+
+Search API Solr Search 1.11 (2016-07-06):
+-----------------------------------------
+- #2710397 by drunken monkey: Fixed escaping of highlighting tags.
+- #2636016 by drunken monkey: Fixed location search distance facets.
+
+Search API Solr Search 1.10 (2016-03-14):
+-----------------------------------------
+- #2604322 by lex0r, drunken monkey: Added the option to log all Solr queries.
+- #2645366 by tedfordgif, drunken monkey: Fixed browser incorrectly filling the
+ HTTP Auth form fields.
+- #2611716 by aditya_anurag, drunken monkey: Improved the method documentation
+ comments.
+- #2599658 by Loparev, drunken monkey: Added the possibility to highlight
+ non-fulltext fields.
+- #2598288 by ethan.han777, JeroenT, drunken monkey: Fixed potential notices in
+ the spellchecker code.
+- #2564927 by thePanz: Added support for random sorting in grouped searches.
+- #2527528 by maximpodorov, drunken monkey: Fixed searching of string fields
+ with leading/trailing spaces.
+- #2513314 by fortis, drunken monkey: Fixed error for empty filters.
+- #2551763 by drunken monkey: Fixed mention of INSTALL.txt in an error message.
+
+Search API Solr Search 1.9 (2015-08-30):
+----------------------------------------
+- #2503829 by das-peter: Added support for grouping on "magic" fields.
+- #2503617 by tobiasb: Fixed undefined "status_message" property in HTTP
+ response object.
+- #2313591 by thePanz, nadavoid: Added support for random sorting.
+- #2004596 by drunken monkey: Fixed workarounds for MLT problems.
+- #2486533 by drunken monkey: Fixed the default operator in the Solr 5 configs.
+- #2466897 by drunken monkey: Fixed Solr version options in server settings.
+- #1918904 by arnested, ramlev, drunken monkey: Added an alter hook for
+ autocomplete suggestions.
+- #2532812 by drunken monkey: Improved performance for filter-only queries.
+- #2463523 by bmunslow: Fixed field boosts in multi-index searches.
+
+Search API Solr Search 1.8 (2015-06-15):
+----------------------------------------
+- #2502511 by drunken monkey: Fixed index removal on Solr 5.
+- #2441117 by drunken monkey: Fixed unnecessary reindexing after changing
+ fields' boosts.
+
+Search API Solr Search 1.7 (2015-06-08):
+----------------------------------------
+- #2466489 by drunken monkey: Changed installation instructions to point to the
+ handbook.
+- #2486533 by drunken monkey: Fixed default operator in Solr 5.
+- #2456159 by drunken monkey: Updated config files to their latest version from
+ the common configs.
+- #2442077 by drunken monkey, basvredeling: Added support for Solr 5.
+- #2451037 by drunken monkey: Fixed timeout errors during indexing.
+- #2054551 by paolomainardi, drunken monkey, lex0r: Added possibility to
+ retrieve Solr field values in multi-index searches.
+- #1908990 by drunken monkey: Fixed various issues with excerpts.
+- #2368399 by das-peter, drunken monkey: Added Support for location filtering
+ with a bounding box.
+- #2333133 by drunken monkey: Fixed behavior of filters on fulltext fields.
+
+Search API Solr Search 1.6 (2014-09-08):
+----------------------------------------
+- #2050961 by das-peter, drunken monkey: Added proximity/distance information
+ to search results.
+- #2242073 by RaF: Fixed handling of custom negative filters in filter-only
+ searches.
+- #2290601 by drunken monkey: Fixed handling of complex keywords and OR facets.
+- #2083357 by drunken monkey: Added note that Solr 4.x server paths should be
+ specified with core.
+- #2270767 by RaF: Fixed search_api_solr_views_data_alter() not always
+ returning all virtual fields.
+
+Search API Solr Search 1.5 (2014-05-23):
+----------------------------------------
+- #2216895 by das-peter: Added support for empty/non-empty conditions on
+ location field types.
+- #2162627 by drunken monkey: Removed Solr 1.4 support.
+- #2175829 by danquah, drunken monkey: Fixed error when admin interface is not
+ accessible.
+- #2222037 by drunken monkey: Fixed "Files" tab in Solr 4.7.
+- #2151719 by Derimagia, drunken monkey: Added an alter hook for multi-index
+ search results.
+- #1776534 by drunken monkey, e2thex: Added support for using a Solr server
+ with multiple sites.
+- #2152337 by drunken monkey: Removed confusing "multiple text fields" section
+ from README.txt.
+- #2099559 by drunken monkey: Made optimizing the Solr server optional.
+- #2146749 by drunken monkey: Added soft commits as the default for Solr 4.
+- #1773440 by drunken monkey: Added performance improvement for “filter
+ only” queries.
+- #2147573 by drunken monkey: Improved error handling.
+
+Search API Solr Search 1.4 (2013-12-25):
+----------------------------------------
+- #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 (2013-10-23):
+----------------------------------------
+- #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 (2013-09-01):
+----------------------------------------
+- #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 (2013-07-21):
+----------------------------------------
+- #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 (2013-06-09):
+----------------------------------------
+- #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 (2013-05-17):
+----------------------------------------------
+- #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 (2013-04-22):
+----------------------------------------------
+- #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 (2013-01-06):
+----------------------------------------------
+- #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 (2012-05-23):
+----------------------------------------------
+- 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 (2011-11-10):
+----------------------------------------------
+- #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 (2011-09-08):
+------------------------------------------------
+- #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 (2011-06-06):
+------------------------------------------------
+- #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 (2011-03-04):
+------------------------------------------------
+- #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 (2010-11-29):
+------------------------------------------------
+Basic functionality is in place and quite well-tested, including support for
+facets and for multi-index searches.
diff --git a/config/search_api_solr.settings.json b/config/search_api_solr.settings.json
index b94f228e..2fb3e403 100644
--- a/config/search_api_solr.settings.json
+++ b/config/search_api_solr.settings.json
@@ -3,6 +3,7 @@
"_module": "search_api_solr",
"search_api_solr_site_hash": "",
"search_api_solr_highlight_prefix": "tm_",
+ "search_api_solr_highlight_method": "original",
"search_api_solr_autocomplete_max_occurrences": "0.9",
"search_api_solr_connection_class": "SearchApiSolrConnection",
"search_api_solr_index_prefix": "",
diff --git a/includes/document.inc b/includes/document.inc
index 43e7963f..a7ca010c 100644
--- a/includes/document.inc
+++ b/includes/document.inc
@@ -1,521 +1,529 @@
-
- */
-
-/**
- * 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();
-
- /**
- * Document field update values, indexed by name.
- *
- * @var array
- */
- protected $fieldUpdates = array();
-
- /**
- * Document nested objects.
- *
- * @var SearchApiSolrDocument[]
- */
- protected $nestedObjects = array();
-
- /**
- * Clears all boosts and fields from this document.
- */
- public function clear() {
- $this->documentBoost = FALSE;
-
- $this->fields = array();
- $this->fieldBoosts = array();
- $this->fieldUpdates = array();
- $this->nestedObjects = 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 currently set field's 'update' attribute for a document field.
- *
- * @param string $key
- * The name of the field.
- *
- * @return string|false
- * The currently set field's update attribute, or FALSE if none was set.
- */
- public function getFieldUpdate($key) {
- return isset($this->fieldUpdates[$key]) ? $this->fieldUpdates[$key] : FALSE;
- }
-
- /**
- * Sets the field's 'update' attribute for a document field.
- *
- * @param string $key
- * The name of the field.
- * @param string|false $update
- * One of the allowed update values ('add', 'set', 'inc').
- */
- public function setFieldUpdate($key, $update) {
- $this->fieldUpdates[$key] = $update;
- }
-
- /**
- * Retrieves all currently set field updates.
- *
- * @return string[]
- * Associative array of field's "update" attributes that were set, keyed by
- * field name.
- */
- public function getFieldUpdates() {
- return $this->fieldUpdates;
- }
-
- /**
- * 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);
- }
-
- /**
- * Retrieves the nested documents set on this document.
- *
- * @return \SearchApiSolrDocument[]
- * The nested documents.
- */
- public function getNestedObjects() {
- return $this->nestedObjects;
- }
-
- /**
- * Sets an array of nested documents.
- *
- * Populate nested documents for use with block join queries. Note that this
- * will lead to errors when used with Solr versions older than 4.5.
- *
- * @param SearchApiSolrDocument[] $nested_documents
- * An array of SearchApiSolrDocument objects.
- */
- public function setNestedDocuments(array $nested_documents) {
- $this->nestedObjects = $nested_documents;
- }
-
- /**
- * 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 => $values) {
- $fieldBoost = $this->getFieldBoost($key);
- $fieldUpdate = $this->getFieldUpdate($key);
- $key = htmlspecialchars($key, ENT_COMPAT, 'UTF-8');
-
- if (!is_array($values)) {
- $values = array($values);
- }
-
- foreach ($values as $value) {
- $xml .= '';
- }
- }
-
- // If nested objects have been added, include them in the XML to be indexed.
- foreach ($this->nestedObjects as $object) {
- // Skip any documents that aren't of the correct type.
- if (!($object instanceof SearchApiSolrDocument)) {
- $vars['@type'] = is_object($object) ? get_class($object) : gettype($object);
- watchdog('search_api_solr', 'Attempt to add an invalid nested Solr document of type @type.', $vars, WATCHDOG_ERROR);
- continue;
- }
- // Generate the markup for each nested document.
- $xml .= $object->toXml();
- }
-
- $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);
- }
-
-}
+>>>>>> 4d02b0e1925e2dfa92a2307d455a45e403a11d68
+/**
+ * Copyright (c) 2007-2009, Conduit Internet Technologies, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * - Neither the name of Conduit Internet Technologies, Inc. nor the names of
+ * its contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @copyright Copyright 2007-2009 Conduit Internet Technologies, Inc. (http://conduit-it.com)
+ * @license New BSD (http://solr-php-client.googlecode.com/svn/trunk/COPYING)
+ * @version $Id: Document.php 15 2009-08-04 17:53:08Z donovan.jimenez $
+ *
+ * @package Apache
+ * @subpackage Solr
+ * @author Donovan Jimenez
+ */
+
+/**
+ * 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();
+
+ /**
+ * Document field update values, indexed by name.
+ *
+ * @var array
+ */
+ protected $fieldUpdates = array();
+
+ /**
+ * Document nested objects.
+ *
+ * @var SearchApiSolrDocument[]
+ */
+ protected $nestedObjects = array();
+
+ /**
+ * Clears all boosts and fields from this document.
+ */
+ public function clear() {
+ $this->documentBoost = FALSE;
+
+ $this->fields = array();
+ $this->fieldBoosts = array();
+ $this->fieldUpdates = array();
+ $this->nestedObjects = 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 currently set field's 'update' attribute for a document field.
+ *
+ * @param string $key
+ * The name of the field.
+ *
+ * @return string|false
+ * The currently set field's update attribute, or FALSE if none was set.
+ */
+ public function getFieldUpdate($key) {
+ return isset($this->fieldUpdates[$key]) ? $this->fieldUpdates[$key] : FALSE;
+ }
+
+ /**
+ * Sets the field's 'update' attribute for a document field.
+ *
+ * @param string $key
+ * The name of the field.
+ * @param string|false $update
+ * One of the allowed update values ('add', 'set', 'inc').
+ */
+ public function setFieldUpdate($key, $update) {
+ $this->fieldUpdates[$key] = $update;
+ }
+
+ /**
+ * Retrieves all currently set field updates.
+ *
+ * @return string[]
+ * Associative array of field's "update" attributes that were set, keyed by
+ * field name.
+ */
+ public function getFieldUpdates() {
+ return $this->fieldUpdates;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Retrieves the nested documents set on this document.
+ *
+ * @return \SearchApiSolrDocument[]
+ * The nested documents.
+ */
+ public function getNestedObjects() {
+ return $this->nestedObjects;
+ }
+
+ /**
+ * Sets an array of nested documents.
+ *
+ * Populate nested documents for use with block join queries. Note that this
+ * will lead to errors when used with Solr versions older than 4.5.
+ *
+ * @param SearchApiSolrDocument[] $nested_documents
+ * An array of SearchApiSolrDocument objects.
+ */
+ public function setNestedDocuments(array $nested_documents) {
+ $this->nestedObjects = $nested_documents;
+ }
+
+ /**
+ * 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.
+ */
+ #[\ReturnTypeWillChange]
+ 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 => $values) {
+ $fieldBoost = $this->getFieldBoost($key);
+ $fieldUpdate = $this->getFieldUpdate($key);
+ $key = htmlspecialchars($key, ENT_COMPAT, 'UTF-8');
+
+ if (!is_array($values)) {
+ $values = array($values);
+ }
+
+ foreach ($values as $value) {
+ $xml .= '';
+ }
+ }
+
+ // If nested objects have been added, include them in the XML to be indexed.
+ foreach ($this->nestedObjects as $object) {
+ // Skip any documents that aren't of the correct type.
+ if (!($object instanceof SearchApiSolrDocument)) {
+ $vars['@type'] = is_object($object) ? get_class($object) : gettype($object);
+ watchdog('search_api_solr', 'Attempt to add an invalid nested Solr document of type @type.', $vars, WATCHDOG_ERROR);
+ continue;
+ }
+ // Generate the markup for each nested document.
+ $xml .= $object->toXml();
+ }
+
+ $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);
+ }
+<<<<<<< HEAD
+=======
+
+>>>>>>> 4d02b0e1925e2dfa92a2307d455a45e403a11d68
+}
diff --git a/includes/service.inc b/includes/service.inc
index f3f807f0..0eb2bca4 100644
--- a/includes/service.inc
+++ b/includes/service.inc
@@ -1,2726 +1,2804 @@
-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,
- 'skip_schema_check' => FALSE,
- 'log_query' => FALSE,
- 'log_response' => FALSE,
- 'commits_disabled' => FALSE,
- 'solr_version' => '',
- 'http_method' => 'AUTO',
- // Default to TRUE for new servers, but to FALSE for existing ones.
- 'clean_ids' => $this->options ? FALSE : TRUE,
- 'site_hash' => $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(backdrop_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'],
- );
-
- if (!$options['site_hash']) {
- $description = t('If you want to index content from multiple sites on a single Solr server, you should enable the multi-site compatibility here. Note, however, that this will completely clear all search indexes (from this site) lying on this server. All content will have to be re-indexed.');
- $form['site_hash_form'] = array(
- '#type' => 'fieldset',
- '#title' => t('Multi-site compatibility'),
- '#description' => $description,
- '#collapsible' => TRUE,
- );
- $form['site_hash_form']['submit'] = array(
- '#type' => 'submit',
- '#value' => t('Turn on multi-site compatibility and clear all indexes'),
- '#submit' => array('_search_api_solr_switch_to_site_hash'),
- );
- }
- $form['site_hash'] = array(
- '#type' => 'value',
- '#value' => $options['site_hash'],
- );
-
- $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. (For Solr versions 4 and above, this should include the name of the core to use.)'),
- '#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'],
- // This prefix with no-op text and password field will keep most browsers
- // from autocompleting these fields, which is hardly ever what the user
- // wants.
- '#prefix' => '',
- );
- $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. Note: Do not use the "Highlighting" processor for the index together with this option – use one or the other.'),
- '#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']['skip_schema_check'] = array(
- '#type' => 'checkbox',
- '#title' => t('Skip schema verification'),
- '#description' => t('Skip the automatic check for schema-compatibility. Use this override if you are seeing an error-message about an incompatible schema.xml configuration file, and you are sure the configuration is compatible.'),
- '#default_value' => $options['skip_schema_check'],
- );
- $form['advanced']['solr_version'] = array(
- '#type' => 'select',
- '#title' => t('Solr version override'),
- '#description' => t('Specify the Solr version manually in case it cannot be retrieved automatically. The version can be found in the Solr admin interface under "Solr Specification Version" or "solr-spec".'),
- '#options' => array(
- '' => t('Determine automatically'),
- '3' => '3.x',
- '4' => '4.x',
- '5' => '5.x',
- ),
- '#default_value' => $options['solr_version'],
- );
- $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',
- ),
- );
- $form['advanced']['log_query'] = array(
- '#type' => 'checkbox',
- '#title' => t('Log search requests'),
- '#description' => t('Log all outgoing Solr search requests.'),
- '#default_value' => $options['log_query'],
- );
- $form['advanced']['log_response'] = array(
- '#type' => 'checkbox',
- '#title' => t('Log search results'),
- '#description' => t('Log all search result responses received from Solr. NOTE: This may slow down your site since all response data (including possible retrieved data) will be saved in the Backdrop log.'),
- '#default_value' => $options['log_response'],
- );
- $form['advanced']['commits_disabled'] = array(
- '#type' => 'checkbox',
- '#title' => t('Disable explicit committing'),
- '#description' => t('Do not send any commit commands to the Solr server.'),
- '#default_value' => $options['commits_disabled'],
- );
-
- 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;
- }
-
- /**
- * {@inheritdoc}
- */
- 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.'));
- }
- }
-
- /**
- * {@inheritdoc}
- */
- 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 = backdrop_map_assoc(array(
- 'search_api_autocomplete',
- 'search_api_between',
- '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',
- 'search_api_random_sort',
- ));
- 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 (empty($this->options['skip_schema_check'])) {
- if (substr($stats_summary['@schema_version'], 0, 10) == 'search-api') {
- backdrop_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('https://www.drupal.org/node/1999310');
- $message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in the handbook for setting up Solr.', $variables);
- backdrop_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) {
- $connection_class = $this->getConnectionClass();
- if (!class_exists($connection_class)) {
- throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $connection_class)));
- }
- $options = $this->options + array('server' => $this->server->machine_name);
- $this->solr = new $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' => $connection_class)));
- }
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function addIndex(SearchApiIndex $index) {
- if (module_exists('search_api_multi') && module_exists('search_api_views')) {
- views_invalidate_cache();
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function fieldsUpdated(SearchApiIndex $index) {
- if (module_exists('search_api_multi') && module_exists('search_api_views')) {
- views_invalidate_cache();
- }
- // Find out if anything changed enough to need re-indexing.
- $old_fields = isset($index->original->options['fields']) ? $index->original->options['fields'] : array();
- $new_fields = isset($index->options['fields']) ? $index->options['fields'] : array();
- if (!$old_fields && !$new_fields) {
- return FALSE;
- }
- if (array_diff_key($old_fields, $new_fields) || array_diff_key($new_fields, $old_fields)) {
- return TRUE;
- }
- $old_field_names = $this->getFieldNames($index->original, TRUE);
- $new_field_names = $this->getFieldNames($index, TRUE);
- return $old_field_names != $new_field_names;
- }
-
- /**
- * {@inheritdoc}
- */
- public function removeIndex($index) {
- if (module_exists('search_api_multi') && module_exists('search_api_views')) {
- views_invalidate_cache();
- }
-
- parent::removeIndex($index);
- }
-
- /**
- * {@inheritdoc}
- */
- public function indexItems(SearchApiIndex $index, array $items) {
- $documents = array();
- $ret = array();
- $index_id = $this->getIndexId($index->machine_name);
- $fields = $this->getFieldNames($index);
- $languages = language_list();
- $base_urls = array();
-
- foreach ($items as $id => $item) {
- $doc = new SearchApiSolrDocument();
- $doc->setField('id', $this->createId($index_id, $id));
- $doc->setField('index_id', $index_id);
- $doc->setField('item_id', $id);
-
- // If multi-site compatibility is enabled, add the site hash and
- // language-specific base URL.
- if (!empty($this->options['site_hash'])) {
- $doc->setField('hash', search_api_solr_site_hash());
- $lang = $item['search_api_language']['value'];
- if (empty($base_urls[$lang])) {
- $url_options = array('absolute' => TRUE);
- if (isset($languages[$lang])) {
- $url_options['language'] = $languages[$lang];
- }
- $base_urls[$lang] = url(NULL, $url_options);
- }
- $doc->setField('site', $base_urls[$lang]);
- }
-
- // Now add all fields contained in the item, with dynamic fields. Also,
- // gather the contents of all text fields to also add them to "content".
- $text_content = array();
- foreach ($item as $key => $field) {
- // If the field is not known for the index, something weird has
- // happened. We refuse to index the items and hope that the others are
- // OK.
- if (!isset($fields[$key])) {
- $type = search_api_get_item_type_info($index->item_type);
- $vars = array(
- '@field' => $key,
- '@type' => $type ? $type['name'] : $index->item_type,
- '@id' => $id,
- );
- watchdog('search_api_solr', 'Error while indexing: Unknown field @field set for @type with ID @id.', $vars, WATCHDOG_WARNING);
- $doc = NULL;
- break;
- }
- $text_content[] = $this->addIndexField($doc, $fields[$key], $field['value'], $field['type']);
- }
- $doc->setField('content', implode("\n\n", array_filter($text_content)));
-
- if ($doc) {
- $documents[] = $doc;
- $ret[] = $id;
- }
- }
-
- // Let other modules alter documents before sending them to solr.
- backdrop_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. Optionally, the site hash is
- * also included.
- *
- * @param string $index_id
- * The search index's machine name.
- * @param mixed $item_id
- * The Search API item ID of the item.
- *
- * @return string
- * The Solr ID to use for this item.
- *
- * @see search_api_solr_site_hash()
- */
- public function createId($index_id, $item_id) {
- $site_hash = !empty($this->options['site_hash']) ? search_api_solr_site_hash() . '-' : '';
- return "$site_hash$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',
- 'search_api_random' => 'random',
- );
-
- // 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.
- backdrop_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) {
- $text_content = '';
- // Don't index empty values (i.e., when field is missing).
- if (!isset($value)) {
- return $text_content;
- }
- if (search_api_is_list_type($type)) {
- $type = substr($type, 5, -1);
- foreach ($value as $v) {
- $text_content .= $this->addIndexField($doc, $key, $v, $type, TRUE) . "\n\n";
- }
- return trim($text_content);
- }
- switch ($type) {
- case 'tokens':
- foreach ($value as $v) {
- $text_content .= $v['value'] . ' ';
- $doc->addField($key, $v['value']);
- }
- return trim($text_content);
- case 'boolean':
- $value = $value ? 'true' : 'false';
- break;
- case 'date':
- $value = is_numeric($value) ? (int) $value : strtotime($value);
- if ($value === FALSE) {
- return $text_content;
- }
- $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);
- }
- if (search_api_is_text_type($type)) {
- $text_content = $value;
- }
- return $text_content;
- }
-
- /**
- * 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.
- *
- * @param array|string $ids
- * Either an array containing the ids of the items that should be deleted,
- * or 'all' if all items should be deleted. Other formats might be
- * recognized by implementing classes, but these are not standardized.
- * @param SearchApiIndex $index
- * The index from which items should be deleted, or NULL if all indexes on
- * this server should be cleared (then, $ids has to be 'all').
- *
- * @throws SearchApiException
- * If an error occurred while trying to delete the items.
- */
- public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
- $this->connect();
- if (is_array($ids)) {
- $index_id = $this->getIndexId($index->machine_name);
- $solr_ids = array();
- foreach ($ids as $id) {
- $solr_ids[] = $this->createId($index_id, $id);
- }
- $this->solr->deleteByMultipleIds($solr_ids);
- }
- else {
- $query = array();
- if ($index) {
- $index_id = $this->getIndexId($index->machine_name);
- $index_id = call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
- $query[] = "index_id:$index_id";
- }
- if (!empty($this->options['site_hash'])) {
- // We don't need to escape the site hash, as that consists only of
- // alphanumeric characters.
- $query[] = 'hash:' . search_api_solr_site_hash();
- }
- if ($ids != 'all') {
- $query[] = $query ? "($ids)" : $ids;
- }
- $this->solr->deleteByQuery($query ? implode(' AND ', $query) : '*:*');
- }
- $this->scheduleCommit();
- }
-
- /**
- * {@inheritdoc}
- */
- 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 = $this->getQueryFields($query);
- // Get the index fields to be able to retrieve boosts.
- $index_fields = $index->getFields() + array(
- 'search_api_relevance' => array('type' => 'decimal', 'indexed' => TRUE),
- 'search_api_id' => array('type' => 'integer', 'indexed' => TRUE),
- );
- $qf = array();
- foreach ($search_fields as $f) {
- $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:' . call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
- if (!empty($this->options['site_hash'])) {
- // We don't need to escape the site hash, as that consists only of
- // alphanumeric characters.
- $fq[] = 'hash:' . search_api_solr_site_hash();
- }
-
- // Extract sort.
- $sort = array();
- foreach ($query->getSort() as $field => $order) {
- $f = $fields[$field];
- if (substr($f, 0, 3) == 'ss_') {
- $f = 'sort_' . substr($f, 3);
- }
-
- // The default Solr schema provides a virtual field named "random_SEED"
- // that can be used to randomly sort the results; the field is available
- // only at query-time.
- if ($field == 'search_api_random') {
- $params = $query->getOption('search_api_random_sort', array());
- // Random seed: getting the value from parameters or computing a new one.
- $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
- $f = 'random_' . $seed;
- }
-
- $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();
- // Solr 4 (before 4.6) has a bug which results in numeric fields not being
- // supported in MLT queries.
- $mlt_no_numeric_fields = FALSE;
- if ($version == 4) {
- $system_info = $this->solr->getSystemInfo();
- $mlt_no_numeric_fields = !isset($system_info->lucene->{'solr-spec-version'}) || version_compare($system_info->lucene->{'solr-spec-version'}, '4.6.0', '<');
- }
- foreach($mlt['fields'] as $f) {
- // Date fields don't seem to be supported at all.
- if ($fields[$f][0] === 'd' || ($mlt_no_numeric_fields && 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->getConnectionClass(), 'phrase'), $id);
- $keys = 'id:' . $id;
- // In (early versions of) Solr 5, facets aren't supported with MLT.
- if ($version >= 5) {
- $facet_params = array();
- }
- }
-
- // Handle spatial filters.
- if ($spatials = $query->getOption('search_api_location')) {
- foreach ($spatials as $i => $spatial) {
- // Spatial options all need a field to do anything.
- if (!isset($spatial['field'])) {
- continue;
- }
-
- unset($radius);
- $field = $fields[$spatial['field']];
- $escaped_field = call_user_func(array($this->getConnectionClass(), 'escapeFieldName'), $field);
-
- // If proper bbox coordinates were given use them to filter.
- if (isset($spatial['bbox'])) {
- if ($version >= 4) {
- $bbox = $spatial['bbox'];
- $fq[] = $escaped_field . ':[' . (float) $bbox['bottom'] . ',' . (float) $bbox['left'] . ' TO ' . (float) $bbox['top'] . ',' . (float) $bbox['right'] . ']';
- }
- else {
- $warnings[] = t('Filtering by a bounding box is not supported in Solr versions below 4.');
- }
- }
-
- // Everything other than a bounding box filter requires a point, so stop
- // here (for this option) if "lat" and "lon" aren't both set.
- if (!isset($spatial['lat']) || !isset($spatial['lon'])) {
- continue;
- }
- $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('/^(?:\{!tag[^}]+\})?' . $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);
- }
- }
-
- // Add parameters to fetch distance, if requested.
- if (!empty($spatial['distance']) && $version >= 4) {
- if (strpos($field, ':') === FALSE) {
- // Add pseudofield with the distance to the result items.
- $location_fields[] = '_' . $field . '_distance_:geodist(' . $field . ',' . $point . ')';
- }
- else {
- $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit');
- watchdog('search_api_solr', "Location distance information can't be added because unclean field identifiers are used.", array(), 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);
- }
-
- // The default Solr schema provides a virtual field named
- // "random_SEED" that can be used to randomly sort the results;
- // the field is available only at query-time.
- if ($group_sort_field == 'search_api_random') {
- $params = $query->getOption('search_api_random_sort', array());
- // Random seed: getting the value from parameters or computing a
- // new one.
- $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
- $f = 'random_' . $seed;
- }
-
- $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';
- }
- if (!empty($location_fields)) {
- $params['fl'] .= ',' . implode(',', $location_fields);
- }
-
- // 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);
- backdrop_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;
- }
-
- backdrop_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'];
- $version = $this->solr->getSolrVersion();
-
- // 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;
- }
- $spatials = $query->getOption('search_api_location');
-
- // 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})) {
- $value = $doc->{$solr_property};
-
- // Date fields need some special treatment to become valid date values
- // (i.e., timestamps) again.
- $first_value = $value;
- while (is_array($first_value)) {
- $first_value = reset($first_value);
- }
- if (isset($field_options[$search_api_property]['type'])
- && search_api_extract_inner_type($field_options[$search_api_property]['type']) === 'date'
- && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $first_value)) {
- $value = is_array($value) ? array_map('strtotime', $value) : strtotime($value);
- }
-
- $result['fields'][$search_api_property] = $value;
- }
- }
-
- // 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'];
-
- // If location based search is enabled ensure the calculated distance is
- // set to the appropriate field. If the calculation wasn't possible add
- // the coordinates to allow calculation.
- if ($spatials) {
- foreach ($spatials as $spatial) {
- if (isset($spatial['field']) && !empty($spatial['distance'])) {
- if ($version >= 4) {
- $doc_field = '_' . $fields[$spatial['field']] . '_distance_';
- if (!empty($doc->{$doc_field})) {
- $results['search_api_location'][$spatial['field']][$result['id']]['distance'] = $doc->{$doc_field};
- }
- }
- }
- }
- }
-
- $index_id = $this->getIndexId($index->machine_name);
- $solr_id = $this->createId($index_id, $result['id']);
- $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields, $this->getQueryFields($query));
- 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, array $fulltext_fields) {
- if (!isset($response->highlighting->$id)) {
- return FALSE;
- }
- $output = '';
- $highlighting = $response->highlighting->$id;
-
- $highlight_fields = !empty($this->options['highlight_data']);
- $create_excerpt = !empty($this->options['excerpt']);
- if (!$highlight_fields && !$create_excerpt) {
- return FALSE;
- }
-
- // Collect highlighted field values for the excerpt and set them in the
- // field values, if requested.
- $excerpt_parts = array();
- $field_mapping = array_flip($field_mapping);
- $fulltext_fields = backdrop_map_assoc($fulltext_fields);
- foreach ($highlighting as $solr_property => $values) {
- $values = (array) $values;
- if (empty($field_mapping[$solr_property])) {
- continue;
- }
- $search_api_property = $field_mapping[$solr_property];
-
- // Only use fields that were actually searched for the excerpt.
- if (isset($fulltext_fields[$search_api_property])) {
- // Remember the highlighted value so we can use it for the excerpt, if
- // requested.
- $excerpt_parts = array_merge($excerpt_parts, $values);
- }
-
- if (!$highlight_fields) {
- continue;
- }
-
- $values = $this->sanitizeHighlightValue($values, $search_api_property);
- // Remove highlight prefixes and suffixes so we can compare values in
- // order to replace the corresponding items.
- $orig_values = preg_replace('#\[(/?)HIGHLIGHT\]#', '', $values);
- $field_values = array();
- if (!empty($fields[$search_api_property])) {
- $field_values = $this->sanitizeHighlightValue($fields[$search_api_property]);
- }
- foreach ($field_values as $delta => $field_value) {
- foreach ($orig_values as $num => $item) {
- if ($item === $field_value) {
- $field_values[$delta] = $this->formatHighlighting($values[$num]);
- $change = TRUE;
- continue 2;
- }
- }
- }
- if (!empty($change)) {
- $fields[$search_api_property] = array(
- '#value' => $field_values,
- '#sanitize_callback' => FALSE,
- );
- }
- }
-
- // Create an excerpt, if requested.
- if ($create_excerpt && $excerpt_parts) {
- $excerpt = array();
- $excerpt_length = 0;
- foreach ($excerpt_parts as $value) {
- // Excerpts don't have HTML (except for the highlighting tags, of
- // course).
- $value = strip_tags($value);
- foreach ($this->extractHighlightingSnippets($value) as $snippet) {
- $excerpt[] = $snippet;
- $excerpt_length += backdrop_strlen($snippet);
- // Restrict ourselves to three snippets or 300 characters.
- if (count($excerpt) >= 3 || $excerpt_length >= 300) {
- break 2;
- }
- }
- }
- if ($excerpt) {
- $output = implode(' … ', $excerpt) . ' …';
- }
- }
-
- return $output;
- }
-
- /**
- * Sanitizes a highlighted field value.
- *
- * @param string|array $value
- * Either a highlighted field value, or an array of such values.
- * @param string|null $field_id
- * (optional) The ID of the field for which this sanitizing occurs, if any.
- *
- * @return string|array
- * The sanitized input.
- */
- protected function sanitizeHighlightValue($value, $field_id = NULL) {
- if (is_array($value)) {
- foreach ($value as $i => $nested_value) {
- $value[$i] = $this->sanitizeHighlightValue($nested_value, $field_id);
- }
- return $value;
- }
- return check_plain(strip_tags($value));
- }
-
- /**
- * Changes highlighting tags from our custom, HTML-safe ones to HTML.
- *
- * @param string|string[] $snippet
- * The snippet(s) to format.
- *
- * @return string|string[]
- * The snippet(s), properly formatted as HTML.
- */
- protected function formatHighlighting($snippet) {
- return preg_replace('#\[(/?)HIGHLIGHT\]#', '<$1strong>', $snippet);
- }
-
- /**
- * Extracts short snippets with highlighting from highlighted field values.
- *
- * @param string $value
- * A highlighted field value.
- *
- * @return string[]
- * An array of short, highlighted snippets extracted from the field value.
- */
- protected function extractHighlightingSnippets($value) {
- $parts = preg_split('#\[/?HIGHLIGHT\]#', $value);
- $num_parts = count($parts);
- if ($num_parts < 3) {
- return array();
- }
-
- $snippets = array();
- $snippet = '';
- $combined_length = 0;
- foreach ($parts as $i => $part) {
- // Is this a match (even) or context (odd)?
- if ($i % 2 === 1) {
- $snippet .= '[HIGHLIGHT]' . $part . '[/HIGHLIGHT]';
- continue;
- }
-
- // If there is less than 60 characters between them, we want to fuse two
- // snippets.
- if ($snippet && backdrop_strlen($part) < 60) {
- $snippet .= $part;
- continue;
- }
-
- // Add a suffix context to the existing snippet.
- if ($snippet) {
- $space = strpos($part, ' ', 25);
- // Fall back to just cutting at an arbitrary position, space or no.
- if ($space === FALSE) {
- $space = 30;
- }
- $snippet .= ' ' . substr($part, 0, $space);
-
- $combined_length += backdrop_strlen($snippet);
- $snippets[] = $this->sanitizeAndFormatExcerptSnippet($snippet);
- $snippet = '';
-
- // Restrict ourselves to three snippets or 300 characters.
- if (count($snippets) >= 3 || $combined_length >= 300) {
- break;
- }
-
- $part = substr($part, $space);
- }
-
- // If there are no more matches, stop.
- if ($num_parts <= $i + 1) {
- break;
- }
-
- // Otherwise, prepare a new prefix for the next match.
- $length = backdrop_strlen($part);
- if ($length > 30) {
- $space = strrpos(substr($part, 0, -25), ' ');
- // Fall back to just cutting at an arbitrary position, space or no.
- if ($space === FALSE) {
- $space = $length - 30;
- }
- $part = substr($part, $space + 1);
- }
- $snippet = $part;
- }
-
- if ($snippet) {
- $snippets[] = $this->sanitizeAndFormatExcerptSnippet($snippet);
- }
-
- return $snippets;
- }
-
- /**
- * 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());
-
- $facet_fields = array();
- $facet_queries = array();
- if (isset($response->facet_counts->facet_fields)) {
- $facet_fields = $response->facet_counts->facet_fields;
- }
- if (isset($response->facet_counts->facet_queries)) {
- $facet_queries = $response->facet_counts->facet_queries;
- }
-
- // The key for the "missing" facet (empty string in the JSON). This will
- // be either "" or "_empty_", depending on the PHP version.
- $empty_key = key((array) json_decode('{"":5}'));
- foreach ($extract_facets as $delta => $info) {
- $field = $fields[$info['field']];
- // Fields that have solr facet queries set need special handling.
- if (!empty($info['solr_facet_query'])) {
- if (!empty($facet_queries)) {
- foreach ($facet_queries as $term => $count) {
- // Strip a leading local param so we can correctly detect the
- // field. E.g. "{!ex=...}" might have been set in getFacetParams()
- // for OR facets.
- $term = preg_replace('/^{[^}]+}/', '', $term);
-
- if (strpos($term, $field . ':') === 0) {
- $term = substr($term, strlen($field) + 1);
- if (!preg_match('/^(?:[(\[][^ ]+ .*[)\]]|".*"|!)$/', $term)) {
- $term = "\"$term\"";
- }
- $facets[$delta][] = array(
- 'filter' => $term,
- 'count' => $count,
- 'solr_facet_query' => TRUE,
- );
- }
- }
- }
- }
- elseif (!empty($facet_fields->$field)) {
- $min_count = $info['min_count'];
- $terms = $facet_fields->$field;
- if ($info['missing']) {
- // We have to correctly incorporate the "missing" term ($empty_key).
- // This will ensure that the term with the least results is dropped,
- // if the limit would be exceeded.
- if (isset($terms->$empty_key)) {
- if ($terms->$empty_key < $min_count) {
- unset($terms->$empty_key);
- }
- else {
- $terms = (array) $terms;
- arsort($terms);
- if ($info['limit'] > 0 && count($terms) > $info['limit']) {
- array_pop($terms);
- }
- }
- }
- }
- elseif (isset($terms->$empty_key)) {
- $terms = clone $terms;
- unset($terms->$empty_key);
- }
- $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_key) {
- $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 ($facet_queries && $query->getOption('search_api_location')) {
- foreach ($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->getConnectionClass(), '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(' 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();
- $prefix = '';
- 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]]);
- }
- }
- elseif ($f instanceof SearchApiQueryFilterInterface) {
- $q = $this->createFilterQueries($f, $solr_fields, $fields);
- if ($filter->getConjunction() != $f->getConjunction() && count($q) > 1) {
- $fq[] = '((' . implode(') ' . $f->getConjunction() . ' (', $q) . '))';
- }
- else {
- $fq = array_merge($fq, $q);
- }
- }
- }
- if (method_exists($filter, 'getTags')) {
- foreach ($filter->getTags() as $tag) {
- $prefix = "{!tag=$tag}";
- // We can only apply one tag per filter.
- break;
- }
- }
- if ($or && count($fq) > 1) {
- $fq = array('((' . implode(') OR (', $fq) . '))');
- }
- if ($prefix) {
- foreach ($fq as $i => $filter) {
- $fq[$i] = $prefix . $filter;
- }
- }
- return $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->getConnectionClass(), 'escapeFieldName'), $field);
- // Special handling for location fields.
- if (isset($field_info['real_type']) && $field_info['real_type'] == 'location') {
- // Empty / non-empty comparison has to take place in one of the subfields
- // of the location field type. These subfields are usually generated with
- // the index and the field type as name suffix.
- // @TODO Do we need to handle other operators / values too?
- if ($value === NULL) {
- $field .= '_0___tdouble';
- }
- }
- if ($value === NULL) {
- return ($operator == '=' ? '*:* AND -' : '') . "$field:[* TO *]";
- }
-
- $type = search_api_extract_inner_type($field_info['type']);
- if (!is_array($value)) {
- $value = $this->formatFilterValue($value, $type);
- }
- else {
- foreach($value as &$val) {
- $val = $this->formatFilterValue($val, $type);
- }
- unset($val);
- }
-
- switch (strtoupper($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 *}";
- case 'BETWEEN':
- return "$field:[{$value[0]} TO {$value[1]}]";
- case 'NOT BETWEEN':
- return "*:* AND -$field:[{$value[0]} TO {$value[1]}]";
-
- 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;
- case 'text':
- return '(' . call_user_func(array($this->getConnectionClass(), 'escape'), $value) . ')';
- }
- return call_user_func(array($this->getConnectionClass(), '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';
- 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') {
- // This tag should automatically be placed on any filters created via
- // this filter by the Facet API integration. We here use it to exclude
- // those filters from the logic creating OR facet filters.
- $tag = 'facet:' . $info['field'];
-
- // Check whether Solr facet queries were set for this facet and use
- // those.
- if (!empty($info['solr_facet_query'])) {
- foreach ($info['solr_facet_query'] as $expression) {
- $facet_params['facet.query'][] = "{!ex=$tag}$field:$expression";
- }
- }
- // Otherwise, add the field through a regular "facet.field" parameter.
- else {
- $facet_params['facet.field'][] = "{!ex=$tag}$field";
- }
- }
- elseif (!empty($info['solr_facet_query'])) {
- // No tagging and excluding like above is necessary as facets are built
- // with all filters applied.
- foreach ($info['solr_facet_query'] as $expression) {
- $facet_params['facet.query'][] = "$field:$expression";
- }
- }
- 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';
- }
- }
-
- 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'] = config_get('search_api_solr.settings', 'search_api_solr_highlight_prefix') . '*';
- $highlight_params['hl.simple.pre'] = '[HIGHLIGHT]';
- $highlight_params['hl.simple.post'] = '[/HIGHLIGHT]';
- $highlight_params['hl.snippets'] = 1;
- $highlight_params['hl.fragsize'] = 0;
- }
-
- 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 = backdrop_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 = backdrop_map_assoc(preg_split('/[-\s():{}\[\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY));
- }
- if (!$keys) {
- $keys = NULL;
- }
-
- // Set searched fields
- $search_fields = $this->getQueryFields($query);
- $qf = array();
- foreach ($search_fields as $f) {
- $qf[] = $fields[$f];
- }
-
- // Extract filters
- $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']);
- $index_id = $this->getIndexId($index->machine_name);
- $fq[] = 'index_id:' . call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
- if (!empty($this->options['site_hash'])) {
- // We don't need to escape the site hash, as that consists only of
- // alphanumeric characters.
- $fq[] = 'hash:' . search_api_solr_site_hash();
- }
-
- // 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'];
-
- $alter_data = array(
- 'search' => $search,
- 'query' => $query,
- 'incomplete_key' => $incomplete_key,
- 'user_input' => $user_input,
- );
-
- for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) {
- try {
- // Send search request
- $this->connect();
- backdrop_alter('search_api_solr_query', $call_args, $query);
- $this->preQuery($call_args, $query);
- $response = $this->solr->search($keys, $params, $http_method);
-
- $alter_data['responses'][] = $response;
-
- 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 * config_get('search_api_solr.settings', 'search_api_solr_autocomplete_max_occurrences');
- 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 (backdrop_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);
- }
-
- backdrop_alter('search_api_solr_autocomplete_suggestions', $suggestions, $alter_data);
-
- return $suggestions;
- }
-
- //
- // SearchApiMultiServiceInterface methods
- //
-
- /**
- * Implements SearchApiMultiServiceInterface::queryMultiple().
- */
- public function queryMultiple(array $options = array()) {
- return search_api_multi_query($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) {
- $boost = isset($fields[$f]['boost']) ? '^' . $fields[$f]['boost'] : '';
- $qf[] = $solr_fields[$f] . $boost;
- }
-
- // Extract filters
- $filter = $query->getFilter();
- $fq = $this->createFilterQueries($filter, $solr_fields, $fields);
-
- // Restrict search to searched indexes.
- $index_filter = array();
- $indexes = array();
- foreach ($query->getIndexes() as $index) {
- $index_id = $this->getIndexId($index->machine_name);
- $indexes[$index_id] = $index;
- $index_filter[] = 'index_id:' . call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
- }
- $fq[] = implode(' OR ', $index_filter);
- if (!empty($this->options['site_hash'])) {
- // We don't need to escape the site hash, as that consists only of
- // alphanumeric characters.
- $fq[] = 'hash:' . search_api_solr_site_hash();
- }
-
- // 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;
- }
- 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';
-
- // Send search request
- $time_processing_done = microtime(TRUE);
- $this->connect();
-
- $call_args = array(
- 'query' => &$keys,
- 'params' => &$params,
- 'http_method' => &$http_method,
- );
- backdrop_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();
- $fulltext_fields_by_index = array();
- foreach ($search_fields as $field) {
- list ($index_id, $field) = explode(':', $field, 2);
- $fulltext_fields_by_index[$index_id][] = $field;
- }
- foreach ($response->response->docs as $id => $doc) {
- $index_id = $doc->index_id;
- if (isset($indexes[$index_id])) {
- $index = $indexes[$index_id];
- }
- else {
- $index = new SearchApiIndex(array('machine_name' => $index_id));
- }
- $fields = $this->getFieldNames($index);
- $field_options = $index->options['fields'];
- $result = array(
- 'id' => NULL,
- 'index_id' => $index_id,
- 'score' => NULL,
- 'fields' => array(),
- );
- $solr_id = $this->createId($index_id, $doc->item_id);
- foreach ($fields as $search_api_property => $solr_property) {
- if (isset($doc->{$solr_property})) {
- $value = $doc->{$solr_property};
-
- // Date fields need some special treatment to become valid date values
- // (i.e., timestamps) again.
- $first_value = $value;
- while (is_array($first_value)) {
- $first_value = reset($first_value);
- }
- if (isset($field_options[$search_api_property]['type'])
- && search_api_extract_inner_type($field_options[$search_api_property]['type']) === 'date'
- && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $first_value)) {
- $value = is_array($value) ? array_map('strtotime', $value) : strtotime($value);
- }
-
- $result['fields'][$search_api_property] = $value;
- }
- }
-
- $fulltext_fields = isset($fulltext_fields_by_index[$index_id]) ? $fulltext_fields_by_index[$index_id] : array();
- $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields, $fulltext_fields);
- if ($excerpt) {
- $result['excerpt'] = $excerpt;
- }
-
- // 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'];
- $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;
- // The key for the "missing" facet (empty string in the JSON). This will
- // be either "" or "_empty_", depending on the PHP version.
- $empty_key = key((array) json_decode('{"":5}'));
- 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 "missing" term ($empty_key).
- // This will ensure that the term with the least results is dropped,
- // if the limit would be exceeded.
- if (isset($terms->$empty_key)) {
- if ($terms->$empty_key < $min_count) {
- unset($terms->$empty_key);
- }
- else {
- $terms = (array) $terms;
- arsort($terms);
- if ($info['limit'] > 0 && count($terms) > $info['limit']) {
- array_pop($terms);
- }
- }
- }
- }
- elseif (isset($terms->$empty_key)) {
- $terms = clone $terms;
- unset($terms->$empty_key);
- }
- $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_key) {
- $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]);
- }
- }
- }
- }
-
- backdrop_alter('search_api_solr_multi_search_results', $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;
- }
-
- //
- // 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() {
- // If committing has been disabled altogether, do nothing here.
- if (!empty($this->options['commits_disabled'])) {
- return;
- }
- 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;
- backdrop_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 settings_get('search_api_solr_connection_class', $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->getConnectionClass() . '::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 string:
- * - If set, the "search_api_solr_index_prefix" 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 environment prefix.
- $id = config_get('search_api_solr.settings', 'search_api_solr_index_prefix') . $machine_name;
-
- return $id;
- }
-
- /**
- * Retrieves the effective fulltext fields from the query.
- *
- * Automatically translates a NULL value in the query object to all fulltext
- * fields in the search index.
- *
- * If a specific backend supports any "virtual" fulltext fields not listed in
- * the index, it should override this method to add them, if appropriate.
- *
- * @param SearchApiQueryInterface $query
- * The search query.
- *
- * @return string[]
- * The fulltext fields in which to search for the search keys.
- *
- * @see SearchApiQueryInterface::getFields()
- */
- protected function getQueryFields(SearchApiQueryInterface $query) {
- $fulltext_fields = $query->getFields();
- $index_fields = $query->getIndex()->getFulltextFields();
- return $fulltext_fields === NULL ? $index_fields : array_intersect($fulltext_fields, $index_fields);
- }
-
- /**
- *
- *
- * @param $snippet
- *
- * @return string|string[]
- */
- protected function sanitizeAndFormatExcerptSnippet($snippet) {
- // Sanitize and format the snippet.
- $snippet = check_plain($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");
- return $snippet;
- }
-
-}
+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,
+ 'skip_schema_check' => FALSE,
+ 'log_query' => FALSE,
+ 'log_response' => FALSE,
+ 'commits_disabled' => FALSE,
+ 'solr_version' => '',
+ 'http_method' => 'AUTO',
+ // Default to TRUE for new servers, but to FALSE for existing ones.
+ 'clean_ids' => $this->options ? FALSE : TRUE,
+ 'site_hash' => $this->options ? FALSE : TRUE,
+ 'autocorrect_spell' => TRUE,
+ 'autocorrect_suggest_words' => TRUE,
+ 'highlight_prefix' => '',
+ 'highlight_suffix' => '',
+ );
+
+ if (!$options['clean_ids']) {
+ if (module_exists('advanced_help')) {
+ $variables['@url'] = url('help/search_api_solr/README.txt');
+ }
+ else {
+ $variables['@url'] = url(backdrop_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'],
+ );
+
+ if (!$options['site_hash']) {
+ $description = t('If you want to index content from multiple sites on a single Solr server, you should enable the multi-site compatibility here. Note, however, that this will completely clear all search indexes (from this site) lying on this server. All content will have to be re-indexed.');
+ $form['site_hash_form'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Multi-site compatibility'),
+ '#description' => $description,
+ '#collapsible' => TRUE,
+ );
+ $form['site_hash_form']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Turn on multi-site compatibility and clear all indexes'),
+ '#submit' => array('_search_api_solr_switch_to_site_hash'),
+ );
+ }
+ $form['site_hash'] = array(
+ '#type' => 'value',
+ '#value' => $options['site_hash'],
+ );
+
+ $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. (For Solr versions 4 and above, this should include the name of the core to use.)'),
+ '#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'],
+ // This prefix with no-op text and password field will keep most browsers
+ // from autocompleting these fields, which is hardly ever what the user
+ // wants.
+ '#prefix' => '',
+ );
+ $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'],
+ );
+ // Make in-built highlighting from the solr server configurable.
+ $form['advanced']['highlight_prefix'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Highlight prefix'),
+ '#default_value' => $options['highlight_prefix'],
+ '#description' => t('Change the prefix if using Solr to perform highlighting (not highlight processor). If empty, defaults to %tag', array('%tag' => '')),
+ '#states' => array(
+ 'invisible' => array(
+ array(
+ ':input[name="options[form][advanced][excerpt]"]' => array(
+ 'checked' => FALSE,
+ ),
+ ),
+ ),
+ ),
+ );
+ $form['advanced']['highlight_suffix'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Highlight suffix'),
+ '#default_value' => $options['highlight_suffix'],
+ '#description' => t('Change the suffix if using Solr to perform highlighting (not highlight processor). If empty, defaults to %tag', array('%tag' => '')),
+ '#states' => array(
+ 'invisible' => array(
+ array(
+ ':input[name="options[form][advanced][excerpt]"]' => array(
+ 'checked' => FALSE,
+ ),
+ ),
+ ),
+ ),
+ );
+ $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. Note: Do not use the "Highlighting" processor for the index together with this option – use one or the other.'),
+ '#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']['skip_schema_check'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Skip schema verification'),
+ '#description' => t('Skip the automatic check for schema-compatibility. Use this override if you are seeing an error-message about an incompatible schema.xml configuration file, and you are sure the configuration is compatible.'),
+ '#default_value' => $options['skip_schema_check'],
+ );
+ $form['advanced']['solr_version'] = array(
+ '#type' => 'select',
+ '#title' => t('Solr version override'),
+ '#description' => t('Specify the Solr version manually in case it cannot be retrieved automatically. The version can be found in the Solr admin interface under "Solr Specification Version" or "solr-spec".'),
+ '#options' => array(
+ '' => t('Determine automatically'),
+ '3' => '3.x',
+ '4' => '4.x',
+ '5' => '5.x',
+ '6' => '6.x',
+ '7' => '7.x',
+ '8' => '8.x',
+ '9' => '9.x',
+ ),
+ '#default_value' => $options['solr_version'],
+ );
+ $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',
+ ),
+ );
+ $form['advanced']['log_query'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Log search requests'),
+ '#description' => t('Log all outgoing Solr search requests.'),
+ '#default_value' => $options['log_query'],
+ );
+ $form['advanced']['log_response'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Log search results'),
+ '#description' => t('Log all search result responses received from Solr. NOTE: This may slow down your site since all response data (including possible retrieved data) will be saved in the Backdrop log.'),
+ '#default_value' => $options['log_response'],
+ );
+ $form['advanced']['commits_disabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disable explicit committing'),
+ '#description' => t('Do not send any commit commands to the Solr server.'),
+ '#default_value' => $options['commits_disabled'],
+ );
+
+ 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;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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 = backdrop_map_assoc(array(
+ 'search_api_autocomplete',
+ 'search_api_between',
+ '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',
+ 'search_api_random_sort',
+ ));
+ 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 (empty($this->options['skip_schema_check'])) {
+ if (substr($stats_summary['@schema_version'], 0, 10) == 'search-api') {
+ backdrop_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('https://www.drupal.org/node/1999310');
+ $message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in the handbook for setting up Solr.', $variables);
+ backdrop_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.
+ *
+ * @param bool $clean_path
+ * A boolean indicating whether to remove # in path.
+ */
+ protected function connect($clean_path = TRUE) {
+ if (!$this->solr) {
+ $connection_class = $this->getConnectionClass();
+ if (!class_exists($connection_class)) {
+ throw new SearchApiException(t('Invalid class @class set as Solr connection class.', array('@class' => $connection_class)));
+ }
+ $options = $this->options + array('server' => $this->server->machine_name);
+
+ // If clean path is set then remove (/#/) character patterns.
+ if ($clean_path && strpos($options['path'], '/#/')) {
+ $alternative_options = $options;
+ $alternative_options['path'] = str_replace('/#/', '/', $alternative_options['path']);
+
+ $this->solr = new $connection_class($alternative_options);
+ if ($this->solr instanceof SearchApiSolrConnectionInterface) {
+ return;
+ }
+ }
+
+ $this->solr = new $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' => $connection_class)));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addIndex(SearchApiIndex $index) {
+ if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+ views_invalidate_cache();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fieldsUpdated(SearchApiIndex $index) {
+ if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+ views_invalidate_cache();
+ }
+ // Find out if anything changed enough to need re-indexing.
+ $old_fields = isset($index->original->options['fields']) ? $index->original->options['fields'] : array();
+ $new_fields = isset($index->options['fields']) ? $index->options['fields'] : array();
+ if (!$old_fields && !$new_fields) {
+ return FALSE;
+ }
+ if (array_diff_key($old_fields, $new_fields) || array_diff_key($new_fields, $old_fields)) {
+ return TRUE;
+ }
+ $old_field_names = $this->getFieldNames($index->original, TRUE);
+ $new_field_names = $this->getFieldNames($index, TRUE);
+ return $old_field_names != $new_field_names;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeIndex($index) {
+ if (module_exists('search_api_multi') && module_exists('search_api_views')) {
+ views_invalidate_cache();
+ }
+
+ parent::removeIndex($index);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexItems(SearchApiIndex $index, array $items) {
+ $documents = array();
+ $ret = array();
+ $index_id = $this->getIndexId($index->machine_name);
+ $fields = $this->getFieldNames($index);
+ $languages = language_list();
+ $base_urls = array();
+
+ foreach ($items as $id => $item) {
+ $doc = new SearchApiSolrDocument();
+ $doc->setField('id', $this->createId($index_id, $id));
+ $doc->setField('index_id', $index_id);
+ $doc->setField('item_id', $id);
+
+ // If multi-site compatibility is enabled, add the site hash and
+ // language-specific base URL.
+ if (!empty($this->options['site_hash'])) {
+ $doc->setField('hash', search_api_solr_site_hash());
+ $lang = $item['search_api_language']['value'];
+ if (empty($base_urls[$lang])) {
+ $url_options = array('absolute' => TRUE);
+ if (isset($languages[$lang])) {
+ $url_options['language'] = $languages[$lang];
+ }
+ $base_urls[$lang] = url(NULL, $url_options);
+ }
+ $doc->setField('site', $base_urls[$lang]);
+ }
+
+ // Now add all fields contained in the item, with dynamic fields. Also,
+ // gather the contents of all text fields to also add them to "content".
+ $text_content = array();
+ foreach ($item as $key => $field) {
+ // If the field is not known for the index, something weird has
+ // happened. We refuse to index the items and hope that the others are
+ // OK.
+ if (!isset($fields[$key])) {
+ $type = search_api_get_item_type_info($index->item_type);
+ $vars = array(
+ '@field' => $key,
+ '@type' => $type ? $type['name'] : $index->item_type,
+ '@id' => $id,
+ );
+ watchdog('search_api_solr', 'Error while indexing: Unknown field @field set for @type with ID @id.', $vars, WATCHDOG_WARNING);
+ $doc = NULL;
+ break;
+ }
+ $text_content[] = $this->addIndexField($doc, $fields[$key], $field['value'], $field['type']);
+ }
+ $doc->setField('content', implode("\n\n", array_filter($text_content)));
+
+ if ($doc) {
+ $documents[] = $doc;
+ $ret[] = $id;
+ }
+ }
+
+ // Let other modules alter documents before sending them to solr.
+ backdrop_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. Optionally, the site hash is
+ * also included.
+ *
+ * @param string $index_id
+ * The search index's machine name.
+ * @param mixed $item_id
+ * The Search API item ID of the item.
+ *
+ * @return string
+ * The Solr ID to use for this item.
+ *
+ * @see search_api_solr_site_hash()
+ */
+ public function createId($index_id, $item_id) {
+ $site_hash = !empty($this->options['site_hash']) ? search_api_solr_site_hash() . '-' : '';
+ return "$site_hash$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',
+ 'search_api_random' => 'random',
+ );
+
+ // 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.
+ backdrop_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) {
+ $text_content = '';
+ // Don't index empty values (i.e., when field is missing).
+ if (!isset($value)) {
+ return $text_content;
+ }
+ if (search_api_is_list_type($type)) {
+ $type = substr($type, 5, -1);
+ foreach ($value as $v) {
+ $text_content .= $this->addIndexField($doc, $key, $v, $type, TRUE) . "\n\n";
+ }
+ return trim($text_content);
+ }
+ switch ($type) {
+ case 'tokens':
+ foreach ($value as $v) {
+ $text_content .= $v['value'] . ' ';
+ $doc->addField($key, $v['value']);
+ }
+ return trim($text_content);
+ case 'boolean':
+ $value = $value ? 'true' : 'false';
+ break;
+ case 'date':
+ $value = is_numeric($value) ? (int) $value : strtotime($value);
+ if ($value === FALSE) {
+ return $text_content;
+ }
+ $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);
+ }
+ if (search_api_is_text_type($type)) {
+ $text_content = $value;
+ }
+ return $text_content;
+ }
+
+ /**
+ * 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.
+ *
+ * @param array|string $ids
+ * Either an array containing the ids of the items that should be deleted,
+ * or 'all' if all items should be deleted. Other formats might be
+ * recognized by implementing classes, but these are not standardized.
+ * @param SearchApiIndex $index
+ * The index from which items should be deleted, or NULL if all indexes on
+ * this server should be cleared (then, $ids has to be 'all').
+ *
+ * @throws SearchApiException
+ * If an error occurred while trying to delete the items.
+ */
+ public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
+ $this->connect();
+ if (is_array($ids)) {
+ $index_id = $this->getIndexId($index->machine_name);
+ $solr_ids = array();
+ foreach ($ids as $id) {
+ $solr_ids[] = $this->createId($index_id, $id);
+ }
+ $this->solr->deleteByMultipleIds($solr_ids);
+ }
+ else {
+ $query = array();
+ if ($index) {
+ $index_id = $this->getIndexId($index->machine_name);
+ $index_id = call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
+ $query[] = "index_id:$index_id";
+ }
+ if (!empty($this->options['site_hash'])) {
+ // We don't need to escape the site hash, as that consists only of
+ // alphanumeric characters.
+ $query[] = 'hash:' . search_api_solr_site_hash();
+ }
+ if ($ids != 'all') {
+ $query[] = $query ? "($ids)" : $ids;
+ }
+ $this->solr->deleteByQuery($query ? implode(' AND ', $query) : '*:*');
+ }
+ $this->scheduleCommit();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ 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 = $this->getQueryFields($query);
+ // Get the index fields to be able to retrieve boosts.
+ $index_fields = $index->getFields() + array(
+ 'search_api_relevance' => array('type' => 'decimal', 'indexed' => TRUE),
+ 'search_api_id' => array('type' => 'integer', 'indexed' => TRUE),
+ );
+ $qf = array();
+ foreach ($search_fields as $f) {
+ $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:' . call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
+ if (!empty($this->options['site_hash'])) {
+ // We don't need to escape the site hash, as that consists only of
+ // alphanumeric characters.
+ $fq[] = 'hash:' . search_api_solr_site_hash();
+ }
+
+ // Extract sort.
+ $sort = array();
+ foreach ($query->getSort() as $field => $order) {
+ $f = $fields[$field];
+ if (substr($f, 0, 3) == 'ss_') {
+ $f = 'sort_' . substr($f, 3);
+ }
+
+ // The default Solr schema provides a virtual field named "random_SEED"
+ // that can be used to randomly sort the results; the field is available
+ // only at query-time.
+ if ($field == 'search_api_random') {
+ $params = $query->getOption('search_api_random_sort', array());
+ // Random seed: getting the value from parameters or computing a new one.
+ $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
+ $f = 'random_' . $seed;
+ }
+
+ $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();
+ // Solr 4 (before 4.6) has a bug which results in numeric fields not being
+ // supported in MLT queries.
+ $mlt_no_numeric_fields = FALSE;
+ if ($version == 4) {
+ $system_info = $this->solr->getSystemInfo();
+ $mlt_no_numeric_fields = !isset($system_info->lucene->{'solr-spec-version'}) || version_compare($system_info->lucene->{'solr-spec-version'}, '4.6.0', '<');
+ }
+ foreach($mlt['fields'] as $f) {
+ // Date fields don't seem to be supported at all.
+ if ($fields[$f][0] === 'd' || ($mlt_no_numeric_fields && 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->getConnectionClass(), 'phrase'), $id);
+ $keys = 'id:' . $id;
+ // In (early versions of) Solr 5, facets aren't supported with MLT.
+ if ($version >= 5) {
+ $facet_params = array();
+ }
+ }
+
+ // Handle spatial filters.
+ if ($spatials = $query->getOption('search_api_location')) {
+ foreach ($spatials as $i => $spatial) {
+ // Spatial options all need a field to do anything.
+ if (!isset($spatial['field']) || !isset($fields[$spatial['field']])) {
+ continue;
+ }
+
+ unset($radius);
+ $field = $fields[$spatial['field']];
+ $escaped_field = call_user_func(array($this->getConnectionClass(), 'escapeFieldName'), $field);
+
+ // If proper bbox coordinates were given use them to filter.
+ if (isset($spatial['bbox'])) {
+ if ($version >= 4) {
+ $bbox = $spatial['bbox'];
+ $fq[] = $escaped_field . ':[' . (float) $bbox['bottom'] . ',' . (float) $bbox['left'] . ' TO ' . (float) $bbox['top'] . ',' . (float) $bbox['right'] . ']';
+ }
+ else {
+ $warnings[] = t('Filtering by a bounding box is not supported in Solr versions below 4.');
+ }
+ }
+
+ // Everything other than a bounding box filter requires a point, so stop
+ // here (for this option) if "lat" and "lon" aren't both set.
+ if (!isset($spatial['lat']) || !isset($spatial['lon'])) {
+ continue;
+ }
+ $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('/^(?:\{!tag[^}]+\})?' . $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);
+ }
+ }
+
+ // Add parameters to fetch distance, if requested.
+ if (!empty($spatial['distance']) && $version >= 4) {
+ if (strpos($field, ':') === FALSE) {
+ // Add pseudofield with the distance to the result items.
+ $location_fields[] = '_' . $field . '_distance_:geodist(' . $field . ',' . $point . ')';
+ }
+ else {
+ $link = l(t('edit server'), 'admin/config/search/search_api/server/' . $this->server->machine_name . '/edit');
+ watchdog('search_api_solr', "Location distance information can't be added because unclean field identifiers are used.", array(), 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);
+ }
+
+ // The default Solr schema provides a virtual field named
+ // "random_SEED" that can be used to randomly sort the results;
+ // the field is available only at query-time.
+ if ($group_sort_field == 'search_api_random') {
+ $params = $query->getOption('search_api_random_sort', array());
+ // Random seed: getting the value from parameters or computing a
+ // new one.
+ $seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
+ $f = 'random_' . $seed;
+ }
+
+ $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';
+ }
+ if (!empty($location_fields)) {
+ $params['fl'] .= ',' . implode(',', $location_fields);
+ }
+
+ // 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);
+ backdrop_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;
+ }
+
+ backdrop_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'];
+ $version = $this->solr->getSolrVersion();
+
+ // 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;
+ }
+ $spatials = $query->getOption('search_api_location');
+
+ // 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})) {
+ $value = $doc->{$solr_property};
+
+ // Date fields need some special treatment to become valid date values
+ // (i.e., timestamps) again.
+ $first_value = $value;
+ while (is_array($first_value)) {
+ $first_value = reset($first_value);
+ }
+ if (isset($field_options[$search_api_property]['type'])
+ && search_api_extract_inner_type($field_options[$search_api_property]['type']) === 'date'
+ && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $first_value)) {
+ $value = is_array($value) ? array_map('strtotime', $value) : strtotime($value);
+ }
+
+ $result['fields'][$search_api_property] = $value;
+ }
+ }
+
+ // 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'];
+
+ // If location based search is enabled ensure the calculated distance is
+ // set to the appropriate field. If the calculation wasn't possible add
+ // the coordinates to allow calculation.
+ if ($spatials) {
+ foreach ($spatials as $spatial) {
+ if (isset($spatial['field']) && !empty($spatial['distance'])) {
+ if ($version >= 4) {
+ $doc_field = '_' . $fields[$spatial['field']] . '_distance_';
+ if (!empty($doc->{$doc_field})) {
+ $results['search_api_location'][$spatial['field']][$result['id']]['distance'] = $doc->{$doc_field};
+ }
+ }
+ }
+ }
+ }
+
+ $index_id = $this->getIndexId($index->machine_name);
+ $solr_id = $this->createId($index_id, $result['id']);
+ $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields, $this->getQueryFields($query));
+ 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, array $fulltext_fields) {
+ if (!isset($response->highlighting->$id)) {
+ return FALSE;
+ }
+ $output = '';
+ $highlighting = $response->highlighting->$id;
+
+ $highlight_fields = !empty($this->options['highlight_data']);
+ $create_excerpt = !empty($this->options['excerpt']);
+ if (!$highlight_fields && !$create_excerpt) {
+ return FALSE;
+ }
+
+ // Collect highlighted field values for the excerpt and set them in the
+ // field values, if requested.
+ $excerpt_parts = array();
+ $field_mapping = array_flip($field_mapping);
+ $fulltext_fields = backdrop_map_assoc($fulltext_fields);
+ foreach ($highlighting as $solr_property => $values) {
+ $values = (array) $values;
+ if (empty($field_mapping[$solr_property])) {
+ continue;
+ }
+ $search_api_property = $field_mapping[$solr_property];
+
+ // Only use fields that were actually searched for the excerpt.
+ if (isset($fulltext_fields[$search_api_property])) {
+ // Remember the highlighted value so we can use it for the excerpt, if
+ // requested.
+ $excerpt_parts = array_merge($excerpt_parts, $values);
+ }
+
+ if (!$highlight_fields) {
+ continue;
+ }
+
+ $values = $this->sanitizeHighlightValue($values, $search_api_property);
+ // Remove highlight prefixes and suffixes so we can compare values in
+ // order to replace the corresponding items.
+ $orig_values = preg_replace('#\[(/?)HIGHLIGHT\]#', '', $values);
+ $field_values = array();
+ if (!empty($fields[$search_api_property])) {
+ $field_values = $this->sanitizeHighlightValue($fields[$search_api_property]);
+ }
+ foreach ($field_values as $delta => $field_value) {
+ foreach ($orig_values as $num => $item) {
+ if ($item === $field_value) {
+ $field_values[$delta] = $this->formatHighlighting($values[$num]);
+ $change = TRUE;
+ continue 2;
+ }
+ }
+ }
+ if (!empty($change)) {
+ $fields[$search_api_property] = array(
+ '#value' => $field_values,
+ '#sanitize_callback' => FALSE,
+ );
+ }
+ }
+
+ // Create an excerpt, if requested.
+ if ($create_excerpt && $excerpt_parts) {
+ $excerpt = array();
+ $excerpt_length = 0;
+ foreach ($excerpt_parts as $value) {
+ // Excerpts don't have HTML (except for the highlighting tags, of
+ // course).
+ $value = strip_tags($value);
+ foreach ($this->extractHighlightingSnippets($value) as $snippet) {
+ $excerpt[] = $snippet;
+ $excerpt_length += backdrop_strlen($snippet);
+ // Restrict ourselves to three snippets or 300 characters.
+ if (count($excerpt) >= 3 || $excerpt_length >= 300) {
+ break 2;
+ }
+ }
+ }
+ if ($excerpt) {
+ $output = implode(' … ', $excerpt) . ' …';
+ }
+ }
+
+ return $output;
+ }
+
+ /**
+ * Sanitizes a highlighted field value.
+ *
+ * @param string|array $value
+ * Either a highlighted field value, or an array of such values.
+ * @param string|null $field_id
+ * (optional) The ID of the field for which this sanitizing occurs, if any.
+ *
+ * @return string|array
+ * The sanitized input.
+ */
+ protected function sanitizeHighlightValue($value, $field_id = NULL) {
+ if (is_array($value)) {
+ foreach ($value as $i => $nested_value) {
+ $value[$i] = $this->sanitizeHighlightValue($nested_value, $field_id);
+ }
+ return $value;
+ }
+ return check_plain(strip_tags($value));
+ }
+
+ /**
+ * Changes highlighting tags from our custom, HTML-safe ones to HTML.
+ *
+ * @param string|string[] $snippet
+ * The snippet(s) to format.
+ *
+ * @return string|string[]
+ * The snippet(s), properly formatted as HTML.
+ */
+ protected function formatHighlighting($snippet) {
+ $search = array('[HIGHLIGHT]', '[/HIGHLIGHT]');
+ return str_replace($search, $this->getHighlightingPrefixSuffix(), $snippet);
+ }
+
+ /**
+ * Returns the prefix and suffix for highlighting matches in the excerpt.
+ *
+ * @return string[]
+ * An array of 2 items, prefix and suffix.
+ */
+ protected function getHighlightingPrefixSuffix() {
+ $prefix = '';
+ $suffix = '';
+ if (!empty($this->options['highlight_prefix'])) {
+ $prefix = $this->options['highlight_prefix'];
+ }
+ if (!empty($this->options['highlight_suffix'])) {
+ $suffix = $this->options['highlight_suffix'];
+ }
+ return array($prefix, $suffix);
+ }
+
+ /**
+ * Extracts short snippets with highlighting from highlighted field values.
+ *
+ * @param string $value
+ * A highlighted field value.
+ *
+ * @return string[]
+ * An array of short, highlighted snippets extracted from the field value.
+ */
+ protected function extractHighlightingSnippets($value) {
+ $parts = preg_split('#\[/?HIGHLIGHT\]#', $value);
+ $num_parts = count($parts);
+ if ($num_parts < 3) {
+ return array();
+ }
+
+ $snippets = array();
+ $snippet = '';
+ $combined_length = 0;
+ foreach ($parts as $i => $part) {
+ // Is this a match (even) or context (odd)?
+ if ($i % 2 === 1) {
+ $snippet .= '[HIGHLIGHT]' . $part . '[/HIGHLIGHT]';
+ continue;
+ }
+
+ // If there is less than 60 characters between them, we want to fuse two
+ // snippets.
+ if ($snippet && backdrop_strlen($part) < 60) {
+ $snippet .= $part;
+ continue;
+ }
+
+ // Add a suffix context to the existing snippet.
+ if ($snippet) {
+ $space = strpos($part, ' ', 25);
+ // Fall back to just cutting at an arbitrary position, space or no.
+ if ($space === FALSE) {
+ $space = 30;
+ }
+ $snippet .= ' ' . substr($part, 0, $space);
+
+ $combined_length += backdrop_strlen($snippet);
+ $snippets[] = $this->sanitizeAndFormatExcerptSnippet($snippet);
+ $snippet = '';
+
+ // Restrict ourselves to three snippets or 300 characters.
+ if (count($snippets) >= 3 || $combined_length >= 300) {
+ break;
+ }
+
+ $part = substr($part, $space);
+ }
+
+ // If there are no more matches, stop.
+ if ($num_parts <= $i + 1) {
+ break;
+ }
+
+ // Otherwise, prepare a new prefix for the next match.
+ $length = backdrop_strlen($part);
+ if ($length > 30) {
+ $space = strrpos(substr($part, 0, -25), ' ');
+ // Fall back to just cutting at an arbitrary position, space or no.
+ if ($space === FALSE) {
+ $space = $length - 30;
+ }
+ $part = substr($part, $space + 1);
+ }
+ $snippet = $part;
+ }
+
+ if ($snippet) {
+ $snippets[] = $this->sanitizeAndFormatExcerptSnippet($snippet);
+ }
+
+ return $snippets;
+ }
+
+ /**
+ * 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());
+
+ $facet_fields = array();
+ $facet_queries = array();
+ if (isset($response->facet_counts->facet_fields)) {
+ $facet_fields = $response->facet_counts->facet_fields;
+ }
+ if (isset($response->facet_counts->facet_queries)) {
+ $facet_queries = $response->facet_counts->facet_queries;
+ }
+
+ // The key for the "missing" facet (empty string in the JSON). This will
+ // be either "" or "_empty_", depending on the PHP version.
+ $empty_key = key((array) json_decode('{"":5}'));
+ foreach ($extract_facets as $delta => $info) {
+ // Skip if empty.
+ if (!isset($info['field']) || !isset($fields[$info['field']])) {
+ continue;
+ }
+
+ $field = $fields[$info['field']];
+ // Fields that have solr facet queries set need special handling.
+ if (!empty($info['solr_facet_query'])) {
+ if (!empty($facet_queries)) {
+ foreach ($facet_queries as $term => $count) {
+ // Strip a leading local param so we can correctly detect the
+ // field. E.g. "{!ex=...}" might have been set in getFacetParams()
+ // for OR facets.
+ $term = preg_replace('/^{[^}]+}/', '', $term);
+
+ if (strpos($term, $field . ':') === 0) {
+ $term = substr($term, strlen($field) + 1);
+ if (!preg_match('/^(?:[(\[][^ ]+ .*[)\]]|".*"|!)$/', $term)) {
+ $term = "\"$term\"";
+ }
+ $facets[$delta][] = array(
+ 'filter' => $term,
+ 'count' => $count,
+ 'solr_facet_query' => TRUE,
+ );
+ }
+ }
+ }
+ }
+ elseif (!empty($facet_fields->$field)) {
+ $min_count = $info['min_count'];
+ $terms = $facet_fields->$field;
+ if ($info['missing']) {
+ // We have to correctly incorporate the "missing" term ($empty_key).
+ // This will ensure that the term with the least results is dropped,
+ // if the limit would be exceeded.
+ if (isset($terms->$empty_key)) {
+ if ($terms->$empty_key < $min_count) {
+ unset($terms->$empty_key);
+ }
+ else {
+ $terms = (array) $terms;
+ arsort($terms);
+ if ($info['limit'] > 0 && count($terms) > $info['limit']) {
+ array_pop($terms);
+ }
+ }
+ }
+ }
+ elseif (isset($terms->$empty_key)) {
+ $terms = clone $terms;
+ unset($terms->$empty_key);
+ }
+ $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_key) {
+ $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 ($facet_queries && $query->getOption('search_api_location')) {
+ foreach ($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->getConnectionClass(), '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(' 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();
+ $prefix = '';
+ 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]]);
+ }
+ }
+ elseif ($f instanceof SearchApiQueryFilterInterface) {
+ $q = $this->createFilterQueries($f, $solr_fields, $fields);
+ if ($filter->getConjunction() != $f->getConjunction() && count($q) > 1) {
+ $fq[] = '((' . implode(') ' . $f->getConjunction() . ' (', $q) . '))';
+ }
+ else {
+ $fq = array_merge($fq, $q);
+ }
+ }
+ }
+ if (method_exists($filter, 'getTags')) {
+ foreach ($filter->getTags() as $tag) {
+ $prefix = "{!tag=$tag}";
+ // We can only apply one tag per filter.
+ break;
+ }
+ }
+ if ($or && count($fq) > 1) {
+ $fq = array('((' . implode(') OR (', $fq) . '))');
+ }
+ if ($prefix) {
+ foreach ($fq as $i => $filter) {
+ $fq[$i] = $prefix . $filter;
+ }
+ }
+ return $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->getConnectionClass(), 'escapeFieldName'), $field);
+ // Special handling for location fields.
+ if (isset($field_info['real_type']) && $field_info['real_type'] == 'location') {
+ // Empty / non-empty comparison has to take place in one of the subfields
+ // of the location field type. These subfields are usually generated with
+ // the index and the field type as name suffix.
+ // @TODO Do we need to handle other operators / values too?
+ if ($value === NULL) {
+ $field .= '_0___tdouble';
+ }
+ }
+ if ($value === NULL) {
+ return ($operator == '=' ? '*:* AND -' : '') . "$field:[* TO *]";
+ }
+
+ $type = search_api_extract_inner_type($field_info['type']);
+ if (!is_array($value)) {
+ $value = $this->formatFilterValue($value, $type);
+ }
+ else {
+ foreach($value as &$val) {
+ $val = $this->formatFilterValue($val, $type);
+ }
+ unset($val);
+ }
+
+ switch (strtoupper($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 *}";
+ case 'BETWEEN':
+ return "$field:[{$value[0]} TO {$value[1]}]";
+ case 'NOT BETWEEN':
+ return "*:* AND -$field:[{$value[0]} TO {$value[1]}]";
+
+ 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;
+ case 'text':
+ return '(' . call_user_func(array($this->getConnectionClass(), 'escape'), $value) . ')';
+ }
+ return call_user_func(array($this->getConnectionClass(), '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';
+ 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') {
+ // This tag should automatically be placed on any filters created via
+ // this filter by the Facet API integration. We here use it to exclude
+ // those filters from the logic creating OR facet filters.
+ $tag = 'facet:' . $info['field'];
+
+ // Check whether Solr facet queries were set for this facet and use
+ // those.
+ if (!empty($info['solr_facet_query'])) {
+ foreach ($info['solr_facet_query'] as $expression) {
+ $facet_params['facet.query'][] = "{!ex=$tag}$field:$expression";
+ }
+ }
+ // Otherwise, add the field through a regular "facet.field" parameter.
+ else {
+ $facet_params['facet.field'][] = "{!ex=$tag}$field";
+ }
+ }
+ elseif (!empty($info['solr_facet_query'])) {
+ // No tagging and excluding like above is necessary as facets are built
+ // with all filters applied.
+ foreach ($info['solr_facet_query'] as $expression) {
+ $facet_params['facet.query'][] = "$field:$expression";
+ }
+ }
+ 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';
+ }
+ }
+
+ 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'] = config_get('search_api_solr.settings', 'search_api_solr_highlight_prefix') . '*';
+ $highlight_params['hl.simple.pre'] = '[HIGHLIGHT]';
+ $highlight_params['hl.simple.post'] = '[/HIGHLIGHT]';
+ $highlight_params['hl.snippets'] = 1;
+ $highlight_params['hl.fragsize'] = 0;
+ $highlight_params['hl.method'] = config_get('search_api_solr.settings', 'search_api_solr_highlight_method', 'original');
+ }
+
+ 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 = backdrop_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 = backdrop_map_assoc(preg_split('/[-\s():{}\[\]\\\\"]+/', $keys, -1, PREG_SPLIT_NO_EMPTY));
+ }
+ if (!$keys) {
+ $keys = NULL;
+ }
+
+ // Set searched fields
+ $search_fields = $this->getQueryFields($query);
+ $qf = array();
+ foreach ($search_fields as $f) {
+ $qf[] = $fields[$f];
+ }
+
+ // Extract filters
+ $fq = $this->createFilterQueries($query->getFilter(), $fields, $index->options['fields']);
+ $index_id = $this->getIndexId($index->machine_name);
+ $fq[] = 'index_id:' . call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
+ if (!empty($this->options['site_hash'])) {
+ // We don't need to escape the site hash, as that consists only of
+ // alphanumeric characters.
+ $fq[] = 'hash:' . search_api_solr_site_hash();
+ }
+
+ // 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'];
+
+ $alter_data = array(
+ 'search' => $search,
+ 'query' => $query,
+ 'incomplete_key' => $incomplete_key,
+ 'user_input' => $user_input,
+ );
+
+ for ($i = 0; $i < ($second_pass ? 2 : 1); ++$i) {
+ try {
+ // Send search request
+ $this->connect();
+ backdrop_alter('search_api_solr_query', $call_args, $query);
+ $this->preQuery($call_args, $query);
+ $response = $this->solr->search($keys, $params, $http_method);
+
+ $alter_data['responses'][] = $response;
+
+ 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 * config_get('search_api_solr.settings', 'search_api_solr_autocomplete_max_occurrences');
+ 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 (backdrop_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);
+ }
+
+ backdrop_alter('search_api_solr_autocomplete_suggestions', $suggestions, $alter_data);
+
+ return $suggestions;
+ }
+
+ //
+ // SearchApiMultiServiceInterface methods
+ //
+
+ /**
+ * Implements SearchApiMultiServiceInterface::queryMultiple().
+ */
+ public function queryMultiple(array $options = array()) {
+ return search_api_multi_query($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) {
+ $boost = isset($fields[$f]['boost']) ? '^' . $fields[$f]['boost'] : '';
+ $qf[] = $solr_fields[$f] . $boost;
+ }
+
+ // Extract filters
+ $filter = $query->getFilter();
+ $fq = $this->createFilterQueries($filter, $solr_fields, $fields);
+
+ // Restrict search to searched indexes.
+ $index_filter = array();
+ $indexes = array();
+ foreach ($query->getIndexes() as $index) {
+ $index_id = $this->getIndexId($index->machine_name);
+ $indexes[$index_id] = $index;
+ $index_filter[] = 'index_id:' . call_user_func(array($this->getConnectionClass(), 'phrase'), $index_id);
+ }
+ $fq[] = implode(' OR ', $index_filter);
+ if (!empty($this->options['site_hash'])) {
+ // We don't need to escape the site hash, as that consists only of
+ // alphanumeric characters.
+ $fq[] = 'hash:' . search_api_solr_site_hash();
+ }
+
+ // 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;
+ }
+ 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';
+
+ // Send search request
+ $time_processing_done = microtime(TRUE);
+ $this->connect();
+
+ $call_args = array(
+ 'query' => &$keys,
+ 'params' => &$params,
+ 'http_method' => &$http_method,
+ );
+ backdrop_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();
+ $fulltext_fields_by_index = array();
+ foreach ($search_fields as $field) {
+ list ($index_id, $field) = explode(':', $field, 2);
+ $fulltext_fields_by_index[$index_id][] = $field;
+ }
+ foreach ($response->response->docs as $id => $doc) {
+ $index_id = $doc->index_id;
+ if (isset($indexes[$index_id])) {
+ $index = $indexes[$index_id];
+ }
+ else {
+ $index = new SearchApiIndex(array('machine_name' => $index_id));
+ }
+ $fields = $this->getFieldNames($index);
+ $field_options = $index->options['fields'];
+ $result = array(
+ 'id' => NULL,
+ 'index_id' => $index_id,
+ 'score' => NULL,
+ 'fields' => array(),
+ );
+ $solr_id = $this->createId($index_id, $doc->item_id);
+ foreach ($fields as $search_api_property => $solr_property) {
+ if (isset($doc->{$solr_property})) {
+ $value = $doc->{$solr_property};
+
+ // Date fields need some special treatment to become valid date values
+ // (i.e., timestamps) again.
+ $first_value = $value;
+ while (is_array($first_value)) {
+ $first_value = reset($first_value);
+ }
+ if (isset($field_options[$search_api_property]['type'])
+ && search_api_extract_inner_type($field_options[$search_api_property]['type']) === 'date'
+ && preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $first_value)) {
+ $value = is_array($value) ? array_map('strtotime', $value) : strtotime($value);
+ }
+
+ $result['fields'][$search_api_property] = $value;
+ }
+ }
+
+ $fulltext_fields = isset($fulltext_fields_by_index[$index_id]) ? $fulltext_fields_by_index[$index_id] : array();
+ $excerpt = $this->getExcerpt($response, $solr_id, $result['fields'], $fields, $fulltext_fields);
+ if ($excerpt) {
+ $result['excerpt'] = $excerpt;
+ }
+
+ // 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'];
+ $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;
+ // The key for the "missing" facet (empty string in the JSON). This will
+ // be either "" or "_empty_", depending on the PHP version.
+ $empty_key = key((array) json_decode('{"":5}'));
+ 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 "missing" term ($empty_key).
+ // This will ensure that the term with the least results is dropped,
+ // if the limit would be exceeded.
+ if (isset($terms->$empty_key)) {
+ if ($terms->$empty_key < $min_count) {
+ unset($terms->$empty_key);
+ }
+ else {
+ $terms = (array) $terms;
+ arsort($terms);
+ if ($info['limit'] > 0 && count($terms) > $info['limit']) {
+ array_pop($terms);
+ }
+ }
+ }
+ }
+ elseif (isset($terms->$empty_key)) {
+ $terms = clone $terms;
+ unset($terms->$empty_key);
+ }
+ $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_key) {
+ $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]);
+ }
+ }
+ }
+ }
+
+ backdrop_alter('search_api_solr_multi_search_results', $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;
+ }
+
+ //
+ // 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(FALSE);
+ return $this->solr->ping();
+ }
+
+ /**
+ * Sends a commit command to the Solr server.
+ */
+ public function commit() {
+ // If committing has been disabled altogether, do nothing here.
+ if (!empty($this->options['commits_disabled'])) {
+ return;
+ }
+ 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;
+ backdrop_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 settings_get('search_api_solr_connection_class', $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(FALSE);
+ 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->getConnectionClass() . '::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 string:
+ * - If set, the "search_api_solr_index_prefix" 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 environment prefix.
+ $id = config_get('search_api_solr.settings', 'search_api_solr_index_prefix') . $machine_name;
+
+ return $id;
+ }
+
+ /**
+ * Retrieves the effective fulltext fields from the query.
+ *
+ * Automatically translates a NULL value in the query object to all fulltext
+ * fields in the search index.
+ *
+ * If a specific backend supports any "virtual" fulltext fields not listed in
+ * the index, it should override this method to add them, if appropriate.
+ *
+ * @param SearchApiQueryInterface $query
+ * The search query.
+ *
+ * @return string[]
+ * The fulltext fields in which to search for the search keys.
+ *
+ * @see SearchApiQueryInterface::getFields()
+ */
+ protected function getQueryFields(SearchApiQueryInterface $query) {
+ $fulltext_fields = $query->getFields();
+ $index_fields = $query->getIndex()->getFulltextFields();
+ return $fulltext_fields === NULL ? $index_fields : array_intersect($fulltext_fields, $index_fields);
+ }
+
+ /**
+ * Sanitize and format the snippet.
+ *
+ * @param $snippet
+ * The snippet.
+ *
+ * @return string|string[]
+ */
+ protected function sanitizeAndFormatExcerptSnippet($snippet) {
+ // Sanitize and format the snippet.
+ $snippet = check_plain($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");
+ return $snippet;
+ }
+
+}
diff --git a/search_api_solr.install b/search_api_solr.install
index 025204ff..3c7217c6 100644
--- a/search_api_solr.install
+++ b/search_api_solr.install
@@ -99,3 +99,16 @@ function search_api_solr_update_1000() {
update_variable_del('search_api_solr_cron_action');
update_variable_del('search_api_solr_last_optimize');
}
+
+/**
+ * Save default setting for highlight method.
+ */
+function search_api_solr_update_1001() {
+ // Migrate variables to config.
+ $config = config('search_api_solr.settings');
+ $config->set('search_api_solr_highlight_method', update_variable_get('search_api_solr_highlight_method', 'original'));
+ $config->save();
+
+ update_variable_del('search_api_solr_highlight_method');
+}
+
diff --git a/search_api_solr.module b/search_api_solr.module
index 860cf7da..b91626e4 100644
--- a/search_api_solr.module
+++ b/search_api_solr.module
@@ -276,7 +276,7 @@ function search_api_solr_server_get_files(SearchApiServer $server, $dir_name = N
// Search for directories and recursively merge directory files.
$files_data = json_decode($response->data, TRUE);
$files_list = $files_data['files'];
- $dir_length = strlen($dir_name) + 1;
+ $dir_length = strlen($dir_name ?: '') + 1;
$result = array('' => array());
foreach ($files_list as $file_name => $file_info) {
diff --git a/solr-conf/8.x/elevate.xml b/solr-conf/8.x/elevate.xml
new file mode 100644
index 00000000..71ea0006
--- /dev/null
+++ b/solr-conf/8.x/elevate.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/solr-conf/8.x/mapping-ISOLatin1Accent.txt b/solr-conf/8.x/mapping-ISOLatin1Accent.txt
new file mode 100644
index 00000000..b92d03c5
--- /dev/null
+++ b/solr-conf/8.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/solr-conf/8.x/protwords.txt b/solr-conf/8.x/protwords.txt
new file mode 100644
index 00000000..cda85814
--- /dev/null
+++ b/solr-conf/8.x/protwords.txt
@@ -0,0 +1,7 @@
+#-----------------------------------------------------------------------
+# This file blocks words from being operated on by the stemmer and word delimiter.
+&
+<
+>
+'
+"
diff --git a/solr-conf/8.x/schema.xml b/solr-conf/8.x/schema.xml
new file mode 100644
index 00000000..d05f4812
--- /dev/null
+++ b/solr-conf/8.x/schema.xml
@@ -0,0 +1,693 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
+
+
+
+
+
+
+
diff --git a/solr-conf/8.x/schema_extra_fields.xml b/solr-conf/8.x/schema_extra_fields.xml
new file mode 100644
index 00000000..02b36555
--- /dev/null
+++ b/solr-conf/8.x/schema_extra_fields.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/solr-conf/8.x/schema_extra_types.xml b/solr-conf/8.x/schema_extra_types.xml
new file mode 100644
index 00000000..bd716b82
--- /dev/null
+++ b/solr-conf/8.x/schema_extra_types.xml
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/solr-conf/8.x/solrconfig.xml b/solr-conf/8.x/solrconfig.xml
new file mode 100644
index 00000000..7d95146e
--- /dev/null
+++ b/solr-conf/8.x/solrconfig.xml
@@ -0,0 +1,1793 @@
+
+
+
+
+
+
+
+
+ ${solr.abortOnConfigurationError:true}
+
+
+ ${solr.luceneMatchVersion:LUCENE_80}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 32
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ ${solr.lock.type:native}
+
+
+
+
+
+ true
+
+
+
+
+ 1
+
+ 0
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${solr.autoCommit.MaxDocs:10000}
+ ${solr.autoCommit.MaxTime:120000}
+
+
+
+
+ ${solr.autoSoftCommit.MaxDocs:2000}
+ ${solr.autoSoftCommit.MaxTime:10000}
+
+
+
+
+
+
+
+
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+
+
+
+ 1024
+
+
+ -1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+ 20
+
+
+ 200
+
+
+
+
+
+
+
+
+
+
+
+ solr rocks010
+
+
+
+
+
+ false
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ explicit
+ json
+ true
+ text
+
+
+
+
+
+
+
+ {!xport}
+ xsort
+ false
+
+
+
+ query
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ edismax
+ content
+ explicit
+ true
+ 0.01
+
+ ${solr.pinkPony.timeAllowed:-1}
+ *:*
+
+
+ false
+
+ true
+ false
+
+ 1
+
+
+ spellcheck
+ elevator
+
+
+
+
+
+
+ content
+ 1
+ 1
+ 3
+ 15
+ 20
+ false
+
+ ${solr.mlt.timeAllowed:2000}
+
+
+
+
+
+
+ content
+ explicit
+ true
+
+
+
+
+
+
+
+ text
+
+
+
+
+
+
+ _src_
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text
+ true
+ ignored_
+
+
+ true
+ links
+ ignored_
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+ default
+ wordbreak
+ false
+ false
+ 1
+ 5
+ 5
+ true
+ true
+ 10
+ 5
+
+
+ spellcheck
+
+
+
+
+
+
+ mySuggester
+ FuzzyLookupFactory
+ DocumentDictionaryFactory
+ cat
+ price
+ string
+
+
+
+
+
+ true
+ 10
+
+
+ suggest
+
+
+
+
+
+
+
+
+
+ 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/solr-conf/8.x/solrconfig_extra.xml b/solr-conf/8.x/solrconfig_extra.xml
new file mode 100644
index 00000000..c5bc3acf
--- /dev/null
+++ b/solr-conf/8.x/solrconfig_extra.xml
@@ -0,0 +1,80 @@
+
+
+
+textSpell
+
+
+
+
+
+ default
+ spell
+ spellchecker
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solr-conf/8.x/solrcore.properties b/solr-conf/8.x/solrcore.properties
new file mode 100644
index 00000000..29dab247
--- /dev/null
+++ b/solr-conf/8.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=8.0
+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/solr-conf/8.x/stopwords.txt b/solr-conf/8.x/stopwords.txt
new file mode 100644
index 00000000..d7f243e4
--- /dev/null
+++ b/solr-conf/8.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/solr-conf/8.x/synonyms.txt b/solr-conf/8.x/synonyms.txt
new file mode 100644
index 00000000..7d22eea6
--- /dev/null
+++ b/solr-conf/8.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/solr-conf/9.x/elevate.xml b/solr-conf/9.x/elevate.xml
new file mode 100644
index 00000000..48f65264
--- /dev/null
+++ b/solr-conf/9.x/elevate.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/solr-conf/9.x/mapping-ISOLatin1Accent.txt b/solr-conf/9.x/mapping-ISOLatin1Accent.txt
new file mode 100644
index 00000000..b92d03c5
--- /dev/null
+++ b/solr-conf/9.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/solr-conf/9.x/protwords.txt b/solr-conf/9.x/protwords.txt
new file mode 100644
index 00000000..cda85814
--- /dev/null
+++ b/solr-conf/9.x/protwords.txt
@@ -0,0 +1,7 @@
+#-----------------------------------------------------------------------
+# This file blocks words from being operated on by the stemmer and word delimiter.
+&
+<
+>
+'
+"
diff --git a/solr-conf/9.x/schema.xml b/solr-conf/9.x/schema.xml
new file mode 100644
index 00000000..adc764b4
--- /dev/null
+++ b/solr-conf/9.x/schema.xml
@@ -0,0 +1,699 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
+
+
+
+
+
+
+
diff --git a/solr-conf/9.x/schema_extra_fields.xml b/solr-conf/9.x/schema_extra_fields.xml
new file mode 100644
index 00000000..02b36555
--- /dev/null
+++ b/solr-conf/9.x/schema_extra_fields.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/solr-conf/9.x/schema_extra_types.xml b/solr-conf/9.x/schema_extra_types.xml
new file mode 100644
index 00000000..bd716b82
--- /dev/null
+++ b/solr-conf/9.x/schema_extra_types.xml
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/solr-conf/9.x/solrconfig.xml b/solr-conf/9.x/solrconfig.xml
new file mode 100644
index 00000000..ae342bbf
--- /dev/null
+++ b/solr-conf/9.x/solrconfig.xml
@@ -0,0 +1,1788 @@
+
+
+
+
+
+
+
+
+ ${solr.luceneMatchVersion:LUCENE_90}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 32
+
+
+
+
+
+
+
+
+
+ 4
+
+
+
+
+
+
+ ${solr.lock.type:native}
+
+
+
+
+
+ true
+
+
+
+
+ 1
+
+ 0
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+ ${solr.ulog.dir:}
+
+
+
+
+ ${solr.autoCommit.MaxDocs:10000}
+ ${solr.autoCommit.MaxTime:120000}
+
+
+
+
+
+ ${solr.autoSoftCommit.MaxDocs:2000}
+ ${solr.autoSoftCommit.MaxTime:10000}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${solr.max.booleanClauses:1024}
+
+
+ ${solr.query.minPrefixLength:-1}
+
+
+
+ -1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+ 20
+
+
+ 200
+
+
+
+
+
+
+
+
+
+
+
+ solr rocks010
+
+
+
+
+
+ false
+
+
+ 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ explicit
+ json
+ true
+ text
+
+
+
+
+
+
+
+ {!xport}
+ xsort
+ false
+
+
+
+ query
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ edismax
+ content
+ explicit
+ true
+ 0.01
+
+ ${solr.pinkPony.timeAllowed:-1}
+ *:*
+
+
+ false
+
+ true
+ false
+
+ 1
+
+
+ spellcheck
+ elevator
+
+
+
+
+
+
+ content
+ 1
+ 1
+ 3
+ 15
+ 20
+ false
+
+ ${solr.mlt.timeAllowed:2000}
+
+
+
+
+
+
+ content
+ explicit
+ true
+
+
+
+
+
+
+
+
+
+ text
+
+
+
+
+
+
+ _src_
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text
+ true
+ ignored_
+
+
+ true
+ links
+ ignored_
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ explicit
+ true
+
+
+
+
+
+
+ ${solr.replication.leader:false}
+ commit
+ startup
+ ${solr.replication.confFiles:schema.xml,mapping-ISOLatin1Accent.txt,protwords.txt,stopwords.txt,synonyms.txt,elevate.xml}
+
+
+ ${solr.replication.follower:false}
+ ${solr.replication.leaderUrl:http://localhost:8983/solr}/replication
+ ${solr.replication.pollInterval:00:00:60}
+
+
+
+
+
+
+ true
+ json
+ true
+
+
+
+
+
+
+
+
+
+ default
+ wordbreak
+ false
+ false
+ 1
+ 5
+ 5
+ true
+ true
+ 10
+ 5
+
+
+ spellcheck
+
+
+
+
+
+
+ mySuggester
+ FuzzyLookupFactory
+ DocumentDictionaryFactory
+ cat
+ price
+ string
+
+
+
+
+
+ true
+ 10
+
+
+ suggest
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ *:*
+
+
+
+
+
+
+
+
+
+
+
+
+ textSpell
+
+
+
+ default
+ spell
+ spellchecker
+ true
+
+
+
+
+
+
diff --git a/solr-conf/9.x/solrconfig_extra.xml b/solr-conf/9.x/solrconfig_extra.xml
new file mode 100644
index 00000000..c5bc3acf
--- /dev/null
+++ b/solr-conf/9.x/solrconfig_extra.xml
@@ -0,0 +1,80 @@
+
+
+
+textSpell
+
+
+
+
+
+ default
+ spell
+ spellchecker
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solr-conf/9.x/solrcore.properties b/solr-conf/9.x/solrcore.properties
new file mode 100644
index 00000000..6ff2cd13
--- /dev/null
+++ b/solr-conf/9.x/solrcore.properties
@@ -0,0 +1,20 @@
+# Defines Solr properties for this specific core.
+solr.replication.leader=false
+solr.replication.follower=false
+solr.replication.pollInterval=00:00:60
+solr.replication.leaderUrl=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=9.0
+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.install.dir=/opt/solr
diff --git a/solr-conf/9.x/stopwords.txt b/solr-conf/9.x/stopwords.txt
new file mode 100644
index 00000000..d7f243e4
--- /dev/null
+++ b/solr-conf/9.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/solr-conf/9.x/synonyms.txt b/solr-conf/9.x/synonyms.txt
new file mode 100644
index 00000000..7d22eea6
--- /dev/null
+++ b/solr-conf/9.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.)