started edlp_search module : displaying a draft of the form

This commit is contained in:
Bachir Soussi Chiadmi
2018-03-19 00:05:20 +01:00
parent 003b381d6d
commit ff495add52
439 changed files with 70494 additions and 120 deletions
@@ -0,0 +1,457 @@
Search API 1.7 (2018-02-23):
----------------------------
- #2922525 by drunken monkey, borisson_: Changed "Index items immediately" to
delay indexing until the end of the page request.
- #2930720 by drunken monkey, borisson_: Added a UI for rebuilding the tracking
table for an index.
- #2912246 by drunken monkey: Fixed inconsistent array indices in query
languages.
- #2939405 by bserem, borisson_, drunken monkey, yoroy: Improved UI text on
what is included in the index.
- #2943705 by kevin.dutra, drunken monkey, borisson_: Fixed performance
problems with LIFO tracker.
- #2942846 by idebr, drunken monkey, borisson_: Added empty/not empty operators
to Views fulltext field filters.
- #2940255 by drunken monkey: Updated the DB autocomplete implementation to the
stable API version.
- #2938646 by drunken monkey, Johnny vd Laar: Fixed item-boosts in database
backend.
- #2939085 by mattgill, drunken monkey: Fixed fatal error in Views when trying
to set keywords conjunction on aborted query.
- #2933811 by drunken monkey: Fixed coding standards in new Drush code.
- #2932347 by drunken monkey, ghaya: Fixed case insensitive matching for
highlighting non-ASCII text.
- #2938288 by drunken monkey: Fixed problems with PHP 7.2.
- #2934321 by Graber, drunken monkey: Fixed Views total for views with pager
offset.
- #2932347 by drunken monkey, ghaya: Fixed case insensitive matching for
highlighting non-ASCII text.
- #2933309 by drunken monkey, mkalkbrenner: Made some helper methods in the
CommandHelper class public.
- #2928279 by drunken monkey, borisson_: Added a test for the default tracker
plugin.
Search API 1.6 (2017-12-24):
----------------------------
- #2669962 by kevin.dutra, drunken monkey, kristofferwiklund, borisson_: Added
option to change the order in which items are indexed.
- #2917399 by drunken monkey, andypost, borisson_, zenimagine: Fixed definition
of Views taxonomy term plugins.
- #2922874 by drunken monkey, borisson_: Fixed DBMS compatibility handling when
a non-default database is used with the Database backend.
- #2923910 by George Bills, drunken monkey, eft, borisson_, robin.ingelbrecht:
Fixed handling of array-valued Views fulltext filter input.
- #2914478 by borisson_, mpp, drunken monkey, pfrenssen, claudiu.cristea,
kevin.dutra, SylvainM, jhedstrom, bircher: Added integration with Drush 9.
- #2926733 by drunken monkey, borisson_: Fixed indexing of leading/trailing
whitespace in fulltext tokens on database backend.
- #2922024 by drunken monkey, borisson_: Fixed Stemmer incorrectly processing
non-English searches.
- #2931730 by drunken monkey: Adapted tests to changes in drupal_set_message().
- #2929739 by drunken monkey, borisson_: Improved adherence to coding standards.
- #2927748 by mkalkbrenner, mstiem, drunken monkey: Fixed Views field handler
for multiple processor-defined fields returned by the server.
- #2928944 by drunken monkey: Removed assert() calls with string parameters.
- #2923976 by Martijn Houtman, drunken monkey: Fixed facets with random sorting
on Database backend.
- #2921582 by chr.fritsch, drunken monkey: Fixed saving of "database_text" form
field for Database servers.
- #2919884 by samuel.mortenson, borisson_, drunken monkey: Improved performance
for saving an index's reindexing state.
- #2855254 by dev.patrick, drunken monkey, borisson_: Removed the remaining
@codingStandardsIgnoreFile directive.
- #2916754 by chr.fritsch, borisson_, drunken monkey: Fixed failure to detect
whether a view is executed in the current request.
- #2910638 by drunken monkey, borisson_, phillipHG: Fixed handling of
entity-valued processor properties.
- #2917036 by drunken monkey, borisson_: Fixed illegal return values in
Database::getAutocompleteSuggestions().
- #2916225 by tilenav, borisson_, drunken monkey: Replaced manual config
setting with installConfig() in our Kernel tests.
- #2917779 by mkalkbrenner, edurenye, drunken monkey: Fixed fatal errors in
views on PHP 5.
- #2905562 by drunken monkey, borisson_: Increased the minimum Core version to
8.4.
- #2905562 by drunken monkey, borisson_: Increased the minimum Core version to
8.4.
- #2916208 by drunken monkey: Fixed AJAX display of plugin forms.
- #2278433 by drunken monkey, borisson_: Added option to Views field handlers
to use highlighted fields data.
- #2278433 by drunken monkey, borisson_: Added option to Views field handlers
to use highlighted fields data.
Search API 1.5 (2017-10-14):
----------------------------
- #2913688 by kevin.dutra, borisson_, drunken monkey: Fixed display plugin for
Views pages with contextual filters.
- #2911734 by drunken monkey, borisson_, mkalkbrenner: Fixed Views display of
fields returned from the backend.
- #2910918 by drunken monkey, borisson_: Fixed error in fields configuration
form.
- #2650986 by drunken monkey, borisson_: Fixed various problems with Views
field handling.
- #2907518 by drunken monkey, jrockowitz, borisson_: Fixed index
creation/update via CLI on large sites.
- #2908440 by claudiu.cristea: Fixed UnsavedIndexConfiguration documentation.
- #2899678 by drunken monkey, Graber, borisson_: Added support for VBO.
- #2909153 by drunken monkey, borisson_: Fixed fatal error for test fails under
certain conditions.
- #2907756 by drunken monkey, bojanz: Fixed "Entity status" processor to
support other publishable entity types.
- #2904377 by drunken monkey, borisson_: Added Autocomplete tests for the DB
backend.
- #2907943 by drunken monkey: Fixed warnings on PHP 5.
- #2906099 by drunken monkey: Fixed use of field's underlying property label in
Views.
- #2896073 by drunken monkey, New Zeal: Fixed warnings when indexing empty text
fields.
- #2898082 by drunken monkey: Re-organized our test class namespaces.
- #2907334 by drunken monkey: Fixed some more Drupal coding standards
violations.
- #2903407 by drunken monkey, alan-ps: Added #optional to form containers where
appropriate.
Search API 1.4 (2017-09-07):
----------------------------
- #2905117 by drunken monkey: Fixed problems with multi-valued
processor-generated fields on Solr.
- #2884034 by drunken monkey: Fixed highlighting for processed keywords.
- #2902947 by drunken monkey: Fixed "Thousands marker" setting for Views fields.
- #2902907 by drunken monkey, juagarc4: Fixed indexing of multibyte text on
MySQL.
- #2903805 by drunken monkey: Disabled fields processors for hidden fields.
- #2903014 by drunken monkey: Fixed "Enable for all fields" processor option.
- #2904976 by drunken monkey: Fixed the current test fails.
- #2902810 by drunken monkey, borisson_: Made query serialization in tests
safer.
- #2903834 by drunken monkey: Adapted to latest changes in Autocomplete module.
- #2903633 by drunken monkey: Fixed current test fails.
- #2899920 by kfritsche: Fixed stored value of boolean Views filters.
- #2896312 by kerasai, drunken monkey: Fixed problems with Views options list
filter.
- #2896878 by drunken monkey: Fixed test fails due to latest Core changes.
- #2665476 by ericgsmith, drunken monkey: Fixed paging in cached views.
Search API 1.3 (2017-07-19):
----------------------------
- #2888584 by drunken monkey: Fixed index description for "cron batch size" of
-1.
- #2895142 by drunken monkey: Made our test classes more useful for other
modules.
- #2851436 by Erik Frèrejean, andywhale, beltofte, drunken monkey: Added a
"Type-specific boosting" processor.
- #2889426 by cspitzlay, drunken monkey: Fixed test fails on Drupal 8.3.
- #2891387 by drunken monkey: Fixed some new code style issues.
- #2891246 by drunken monkey: Fixed error detection in plugin helper.
- #2884451 by drunken monkey: Fixed missing primary key for denormalized index
tables in DB backend.
- #2886978 by drunken monkey: Fixed tracking of entities in disabled languages.
Search API 1.2 (2017-06-25):
----------------------------
- #2881198 by jzavrl, drunken monkey, borisson_: Added property paths to "Add
fields" form.
- #2883475 by Matthijs, drunken monkey, borisson_: Added support for "Language
not applicable".
- #2883807 by LammensJ, drunken monkey: Fixed warning in Views UI for
contextual filters.
- #2880239 by acbramley, drunken monkey, borisson_: Fixed logic for multiple
sorts on the same field.
- #2889426 by drunken monkey: Fixed test fails for latest Core changes.
- #2884720 by blake.thompson: Added a display plugin for the Views Embed
display type.
- #2882347 by mkalkbrenner: Fixed forms to not have unserializable properties.
- #2676468 by acbramley, drunken monkey, borisson_: Fixed stale cache in search
view with tag-based caching.
- #2876398 by drunken monkey: Fixed operator description of Views "Fulltext
search" filter.
- #2881945 by drunken monkey: Removed unnecessary check in
AddURL::addFieldValues().
- #2886981 by drunken monkey: Fixed test fails for latest Core changes.
- #2880026 by Anton4yk, drunken monkey: Fixed bugs in entity datasource's item
discovery code for edge cases.
- #2881631 by jzavrl, drunken monkey, borisson_: Added alphabetic sort for
properties in "Add fields" form.
- #2867809 by drunken monkey, borisson_: Added an option to enable a processor
for all compatible fields.
- #2878974 by acbramley: Added the date Views argument plugin.
- #2840274 by drunken monkey, borisson_: Updated the Core dependency to 8.3.
- #2874895 by harsha012, drunken monkey, borisson_: Fixed OOP code to use the
t() method instead of the global t() function.
- #2624876 by drunken monkey, borisson_: Added a query option for "properties
to retrieve".
- #2682949 by drunken monkey, borisson_: Adapted to changes in the Autocomplete
module.
Search API 1.1 (2017-05-10):
----------------------------
- #2858303 by beluoctavian, drunken monkey: Fixed "is empty" Views filters for
taxonomy term references.
- #2873246 by drunken monkey: Fixed a second error for cached aborted search
queries in Views.
- #2862289 by hoebekewim, drunken monkey: Fixed oversized column for fulltext
fields in denormalized index table.
- #2859683 by drunken monkey: Added a note to fields processors explaining that
per-field keywords processing isn't supported.
- #2871497 by drunken monkey: Fixed validation of "Whitespace characters"
setting for the Tokenizer processor.
- #2868704 by drunken monkey: Fixed old removed fields being present on query
object.
- #2863955 by alan-ps, drunken monkey, borisson_: Fixed unsupported processors
remaining enabled.
- #2870988 by sahilsharma011, c.nish2k3, kala4ek: Removed translations from all
tests.
- #2871030 by drunken monkey: Fixed error for cached aborted search queries in
Views.
- #2230935 by shkiper, drunken monkey: Added the "search-api-server-clear"
Drush command.
Search API 1.0 (2017-04-26):
----------------------------
- #2871145 by opdavies, drunken monkey, borisson_: Fixed link to php.net for
PCRE reference.
- #2543472 by drunken monkey: Fixed indexing of multiple indexes via Drush.
- #2871549 by drunken monkey: Added note about backwards compatibility to
README.txt.
Search API 1.0, RC 4 (2017-04-21):
----------------------------------
- #2869121 by drunken monkey: Fixed fatal error when required fulltext filter is
in exposed form block.
Search API 1.0, RC 3 (2017-04-20):
----------------------------------
- #2869121 by drunken monkey, Wim Leers, wouter.adem, borisson_: Added improved
"Required" handling for the Views "Fulltext search" filter.
- #2870782 by drunken monkey: Fixed the tests for Drupal 8.2.
- #2868851 by acbramley, drunken monkey: Fixed wrong interface used for loggers.
- #2868427 by dbjpanda, drunken monkey: Fixed use of d.o URL alias in
README.txt.
Search API 1.0, RC 2 (2017-04-10):
----------------------------------
- #2846932 by drunken monkey, killua99, borisson_: Fixed error when changing
boost values with a Postgres database backend.
- #2866454 by phenaproxima, drunken monkey: Fixed problems in update 8103.
- #2844945 by drunken monkey: Fixed uncaught exception when adding too many
fields.
Search API 1.0, RC 1 (2017-04-09):
----------------------------------
- #2776659 by drunken monkey, drholera: Removed the deprecated Utility methods.
- #2268809 by drunken monkey: Converted all arrays in the code to use the short
syntax.
- #2863736 by fran seva, drunken monkey: Added a setDataDefinition() method to
the Field class.
- #2867118 by drunken monkey: Fixed reported coding standards problems.
- #2839932 by drunken monkey, borisson_, bmcclure, jacktonkin: Fixed Views
problems with "Rendered item" fields.
- #2765317 by vasike: Added a "Last" aggregation for aggregated fields.
- #2861657 by drunken monkey: Removed remaining usages of "e.g.".
Search API 1.0, Beta 5 (2017-04-02):
------------------------------------
- #2842029 by vasike, drunken monkey: Added a "plugin helper" service for
creating index plugins.
- #2863253 by drunken monkey: Added hook infos for all our hooks.
- #2856050 by StryKaizer, drunken monkey, marthinal, borisson_: Added getPath()
to display plugins and deprecated getUrl().
- #2856003 by borisson_, drunken monkey: Added "index" and "path" to search
display annotation definition.
- #2842557 by StryKaizer, Boobaa, drunken monkey, borisson_: Fixed "is rendered
on current page" checks for Views displays with contextual filters.
- #2855157 by dbjpanda: Fixed a small CSS error.
- #2842007 by shashank.mundhra: Fixed DB comment of search_api_item.status
column.
- #2682369 by Alumei, drunken monkey, swentel, Crell, stBorchert, prics,
borisson_: Fixed problems with overridden config entities.
- #2645882 by drunken monkey, borisson_: Fixed "items could not be indexed"
message for "Index now".
- #2641388 by mallezie, drunken monkey, borisson_, janusman: Added various UX
improvements for the "Fields" tab.
- #2861587 by alexpott: Fixed the DB/Defaults integration tests.
- #2855758 by StryKaizer, drunken monkey: Fixed "is rendered" checks for Views
block displays.
- #2855444 by drunken monkey: Added language-specific test for the "Rendered
item" processor.
- #2794295 by isramv, drunken monkey, alan-ps: Fixed default index indexing term
names instead of IDs.
- #2343161 by alan-ps, drunken monkey: Added per-datasource indexing stats.
- #2857017 by shkiper: Fixed copy-paste errors in js/index-active-formatters.js.
- #2311039 by drunken monkey: Fixed missing dependency injection for all form
and plugin classes.
- #2745655 by drunken monkey: Added test for (NOT) NULL conditions on fulltext
fields in DB backend.
- #2574889 by drunken monkey, ChristianAdamski: Added Tour module integration.
- #2814925 by kducharm, drunken monkey, Cyberwolf: Fixed indexing of computed
fields.
- #2852807 by alan-ps: Fixed CheckStyle warnings in this project.
- #2659868 by borisson_, drunken monkey: Fixed CacheabilityTest to make sure it
displays the rendered entities.
- #2853049 by Blanca.Esqueda: Fixed config schema error for Views date filters.
- #2847810 by JayKandari: Added Stemmer to ProcessorIntegrationTest.
- #2753667 by pfrenssen, drunken monkey, borisson_, idimopoulos, sandervd,
sardara: Improved Views cache plugins and their cache metadata.
- #2843854 by becw, drunken monkey: Fixed date filters in search views.
- #2753763 by drholera, drunken monkey: Added a logging trait which can also
log exceptions.
- #2850025 by alan-ps: Added a "label_collection" property for our entity types.
- #2851533 by drunken monkey: Adapted the tests to the latest Core changes.
- #2491175 by drunken monkey, ptmkenny: Added a new "Entity status" processor
to replace "Node status".
- #2681273 by drunken monkey, borisson_: Added plugin descriptions consistently
to the UI.
- #2642792 by drunken monkey: Fixed and expanded the RenderedItemTest.
- #2844527 by ayalon, drunken monkey: Added some contextual filters for Views.
- #2847307 by JayKandari, drunken monkey: Removed "Display" suffix from our
search display plugins.
- #2849507 by drunken monkey: Fixed test fails on Drupal 8.4.
- #2846255 by drunken monkey: Fixed displayed order of processors in UI.
- #2656916 by drunken monkey: Removed unused field dependency calculation
method.
- #2844192 by BR0kEN, drunken monkey: Fixed failure in DB backend when setting
field boost from 0 to other value.
- #2847588 by Nick_vh, drunken monkey: Improved the description of the
search_api_views_taxonomy module.
- #2843632 by beram, drunken monkey: Fixed "contains any" for Views fulltext
contextual filter.
- #2842971 by jespermb, borisson_: Fixed wrong URL for block search displays.
- #2842546 by alan-ps: Fixed Drush commands for enabling and disabling indexes.
- #2840675 by nicrodgers, drunken monkey: Added support for Views filters with
option lists.
- #2841550 by drunken monkey: Fixed execution of pending tasks.
- #2841827 by umed91, drunken monkey: Fixed some code style issues.
- #2682615 by drunken monkey: Added tests for object serialization and cloning.
- #2794879 by drunken monkey: Added property paths to the "Fields" tab.
- #2840261 by alan-ps: Fixed usage of outdated hash functions.
Search API 1.0, Beta 4 (2016-10-24):
------------------------------------
- #2839981 by borisson_, drunken monkey: Fixed search ID for non-page views.
- #2834350 by alan-ps: Fixed missing field restriction in DB backend
autocomplete code.
- #2809469 by StryKaizer, borisson_, drunken monkey, pfrenssen: Fixed
incompatible cache modes used for Search API views.
- #2839142 by borisson_, drunken monkey: Added new test for Defaults module
index creation.
- #2831436 by borisson_, drunken monkey: Added dependency information to
display plugins.
- #2836994 by dermario: Fixed order of facets in DB backend unreliable.
- #2839445 by borisson_: Fixed test failure in for some Core versions.
- #2833229 by timcosgrove: Removed array type hinting for batch contexts.
- #2656052 by drunken monkey: Removed the "plugin_id"/"settings" sub-level of
index plugin settings.
- #2837099 by drunken monkey: Fixed the tests in latest Drupal 8.3 dev version.
- #2709351 by drunken monkey: Fixed invalid processor configurations when field
configuration changes.
- #2829351 by SchnWalter, drunken monkey, borisson_: Fixed
$entity->search_api_skip_tracking for nodes.
- #2827961 by drunken monkey, Steven Jones: Fixed problems with giant scores in
DB backend.
- #2823985 by felribeiro, dermario, borisson_: Added an integration test for
the "Role filter" processor.
- #2832015 by mkalkbrenner: Removed translations from functional tests.
- #2828184 by ekes: Fixed bug in Stemmer config form validation.
- #2828848 by mkalkbrenner, drunken monkey, borisson_: Prepared IntegrationTest
to be executed with different backends.
- #2828148 by ekes, borisson_, drunken monkey: Fixed problem when stemming
multiple words at once.
- #2826822 by drunken monkey: Fixed use of illegal typed data type "text".
- #2826308 by drunken monkey, mkalkbrenner: Fixed unwanted filter in Views
tests.
- #2734897 by niko-, drunken monkey: Added tests for preIndexSave()
implementations.
- #2650364 by drunken monkey: Added a "skip access checks" option to the Views
relationship plugin.
- #2826160 by keboca, drunken monkey: Fixed config form of "Role filter"
processor to be more robust.
- #2753815 by niko-, drunken monkey, itsekhmistro, borisson_: Ported Simpletest
web tests to BrowserTestBase tests.
- #2358065 by tstoeckler: Added the option for highlighting of partial matches
to the processor.
- #2470837 by sinn, drunken monkey: Added documentation on why to disable Core
Search.
- #2767609 by drunken monkey, borisson_: Added backend tests for empty value
conditions.
- #2789431 by phenaproxima, drunken monkey: Added possibility to display
processor-generated fields in Views.
- #2822553 by niko-: Removed a test assertion that failed when run via GUI.
- #2821445 by drunken monkey: Fixed warning in HTML filter for non-HTML fields.
- #2733185 by sinn, drunken monkey: Added documentation and tests for tracker
and display plugin alter hooks.
- #2824326 by Jo Fitzgerald: Fixed generated autocomplete suggestions of the DB
backend.
- #2824932 by mkalkbrenner: Fixed incorrect indexing call in backend tests.
- #2821498 by stBorchert: Increased our required minimum Core version to 8.2.
- #2816979 by drunken monkey: Added click-sorting for indexed fields in Views.
- #2779159 by mark_fullmer, drunken monkey, borisson_: Added a Stemmer
processor.
- #2799497 by drunken monkey: Added a getter for the Views query's "where"
property.
- #2574583 by drunken monkey: Fixed loading of entities from excluded bundles.
- #2795861 by sinn: Removed some deprecated methods.
- #2748323 by sinn: Fixed comment reference to removed method
alterPropertyDefinitions().
- #2809753 by drunken monkey: Fixed issues with multiple OR facets.
- #2821955 by mkalkbrenner: Adapted processor test base for use with other
backends.
- #2819637 by alan-ps: Renamed use of the DB layer rollback() method to
rollBack().
Search API 1.0, Beta 3 (2016-10-24):
------------------------------------
- #2625152 by jhedstrom, drunken monkey, borisson_, mpp, stijn.blomme,
Rodlangh: Added an "Index hierarchy" processor.
- #2818621 by alan-ps: Fixed overly accurate index status percentage.
- #2792277 by drunken monkey: Fixed issues during config syncing of our
entities.
- #2813525 by drunken monkey, alan-ps: Fixed incorrect indexing of
nodes/comments with excluded bundle.
- #2803701 by drunken monkey, rbayliss: Fixed strict warnings from
UnsavedConfigurationFormTrait.
- #2711017 by drunken monkey: Adapted Core's UncacheableDependencyTrait.
- #2690229 by drunken monkey: Adapted Core's SubformState solution.
- #2575641 by rbayliss, dazz, drunken monkey: Fixed behavior of "Save and edit"
button for indexes.
- #2769021 by drunken monkey: Added the generated Search API query to the Views
preview.
- #2817341 by mkalkbrenner, drunken monkey: Added PluginDependencyTrait to
ConfigurablePluginBase.
- #2809211 by cristiroma: Fixed size of text fields on "Fields" tab.
- #2684465 by Dropa, david.gil, drunken monkey: Fixed indexing of
non-translated entity references.
- #2695627 by dermario, drunken monkey: Added support for (NOT) IN and (NOT)
BETWEEN operators to Views.
- #2782577 by drunken monkey, zuhair_ak: Fixed extraction of configurable
properties in processors.
Search API 1.0, Beta 2 (2016-09-28):
------------------------------------
- #2798643 by drunken monkey, Berdir: Fixed handling of enforced dependencies
for search indexes.
- #2799475 by borisson_, drunken monkey: Added support for Views block and REST
displays in the Views search display deriver.
- #2763161 by drunken monkey, borisson_: Fixed cache issues with Views search
display plugins.
- #2800011 by drunken monkey, borisson_: Fixed display of hidden properties
when adding fields.
- #2794093 by drunken monkey, borisson_, kamalrajsahu21: Fixed the processor
reordering CSS.
- #2640982 by drunken monkey, borisson_: Fixed "unsaved changes" code in the
Fields UI.
- #2727697 by drunken monkey, borisson_: Fixed serialization of modified
indexes.
- #2747767 by joachim: Changed the "Aggregation type" form element to radios.
- #2565621 by LKS90, drunken monkey: Added a test for the database defaults
submodule.
- #2684465 by drunken monkey, marthinal: Fixed indexing of related entities on
multilingual sites.
- #2566241 by drunken monkey: Fixed index tracker select default value.
- #2555177 by drunken monkey: Fixed empty bundle selects in datasource config
forms.
Search API 1.0, Beta 1 (2016-09-05):
------------------------------------
First Beta release of the project's Drupal 8 version. The API can be considered
mostly stable and an upgrade path will be provided for all data structure
changes from this point forward.
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,112 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Requirements
* Installation
* Configuration
* Developers
* Maintainers
INTRODUCTION
------------
This module provides a framework for easily creating searches on any entity
known to Drupal, using any kind of search engine. For site administrators,
it is a great alternative to other search solutions, since it already
incorporates faceting support (with [1]) and the ability to use the Views module
for displaying search results, filters, etc. Also, with the Apache Solr
integration [2], a high-performance search engine is available for this module.
[1] https://www.drupal.org/project/facets
[2] https://www.drupal.org/project/search_api_solr
Developers, on the other hand, will be impressed by the large flexibility and
numerous ways of extension the module provides. Hence, the growing number of
additional contrib modules, providing additional functionality or helping users
customize some aspects of the search process.
* For a full description of the module, visit the project page:
https://www.drupal.org/project/search_api
* To submit bug reports and feature suggestions, or to track changes:
https://www.drupal.org/project/issues/search_api
REQUIREMENTS
------------
No other modules are required.
INSTALLATION
------------
Install as you would normally install a contributed Drupal module. For further
information, see:
https://www.drupal.org/docs/8/extending-drupal-8/installing-modules
CONFIGURATION
-------------
After installation, for a quick start, just install the "Database Search
Defaults" module provided with this project. This will automatically set up a
search view for node content, using a database server for indexing.
Otherwise, you need to enable at least a module providing integration with a
search backend (like database, Solr, Elasticsearch, …). Possible options are
listed at [3].
Then, go to
/admin/config/search/search-api
on your site and create a search server and search index. Afterwards, you can
create a view based on your index to enable users to search the content you
configured to be indexed. More details are available online in the handbook [4].
There, you can also find answers to frequently asked questions and common
pitfalls to avoid.
[3] https://www.drupal.org/docs/8/modules/search-api/getting-started/server-backends-and-features
[4] https://www.drupal.org/docs/8/modules/search-api/getting-started
DEVELOPERS
----------
The Search API provides a lot of ways for developers to extend or customize the
framework.
- Hooks
All available hooks are listed in search_api.api.php.
- Events
Currently, only the Search API's task system (for reliably executing necessary
system tasks) makes use of events. Every time a task is executed, an event
will be fired based on the task's type and the sub-system that scheduled the
task is responsible for reacting to it. This system is extensible and can
therefore also easily be used by contrib modules based on the Search API. For
details, see the description of the \Drupal\search_api\Task\TaskManager class,
and the other classes in src/Task for examples.
- Plugins
The Search API defines several plugin types, all listed in its
search_api.plugin_type.yml file. Here is a list of them, along with the
directory in which you can find there definition files (interface, plugin base
and plugin manager):
- Backends: src/Backend
- Datasources: src/Datasource
- Data types: src/DataType
- Displays: src/Display
- ParseModes: src/ParseMode
- Processors: src/Processor
- Trackers: src/Tracker
The display plugins are a bit of a special case there, because they aren't
really "extending" the framework, but are rather a way of telling the Search
API (and all modules integrating with it) about search pages your module
defines. They can then be used to provide, for example, faceting support for
those pages. Therefore, if your module provides any search pages, it's a good
idea to provide display plugins for them. For an example (for Views pages),
see \Drupal\search_api\Plugin\search_api\display\ViewsPage.
The handbook documentation for developers is available at [5].
[5] https://www.drupal.org/docs/8/modules/search-api/developer-documentation
To know which parts of the module can be relied upon as its public API, please
read the "Drupal 8 backwards compatibility and internal API policy" [6] and the
module's issue regarding potential module-specific changes to that policy [7].
[6] https://www.drupal.org/core/d8-bc-policy
[7] https://www.drupal.org/node/2871549
MAINTAINERS
-----------
Current maintainers:
* Thomas Seidl (drunken monkey) - https://www.drupal.org/u/drunken-monkey
@@ -0,0 +1,36 @@
{
"name": "drupal/search_api",
"description": "Provides a generic framework for modules offering search capabilities.",
"type": "drupal-module",
"homepage": "https://www.drupal.org/project/search_api",
"authors": [
{
"name": "Thomas Seidl",
"homepage": "https://www.drupal.org/u/drunken-monkey"
},
{
"name": "Nick Veenhof",
"homepage": "https://www.drupal.org/u/nick_vh"
},
{
"name": "See other contributors",
"homepage":"https://www.drupal.org/node/790418/committers"
}
],
"support": {
"issues": "https://www.drupal.org/project/issues/search_api",
"irc": "irc://irc.freenode.org/drupal-search-api",
"source": "http://git.drupal.org/project/search_api.git"
},
"license": "GPL-2.0+",
"require-dev": {
"drupal/search_api_autocomplete": "@dev"
},
"extra": {
"drush": {
"services": {
"drush.services.yml": "^9"
}
}
}
}
@@ -0,0 +1,4 @@
default_cron_limit: 50
cron_worker_runtime: 15
default_tracker: 'default'
tracking_page_size: 100
@@ -0,0 +1,96 @@
id: search-api-index-fields
module: search_api
label: Fields indexed in this index
langcode: en
routes:
- route_name: entity.search_api_index.fields
dependencies:
module:
- search_api
tips:
search-api-index-fields-introduction:
id: search-api-index-fields-introduction
plugin: text
label: Fields indexed in this index
body: This page lists which fields are indexed in this index, grouped by datasource. (Datasource-independent fields are listed under "General".) Indexed fields can be used to add filters or sorting to views or other search displays based on the index. Fields with type "Fulltext" can also be used for fulltext searching.
weight: 1
search-api-index-fields-add:
id: search-api-index-fields-add
plugin: text
label: Add fields
body: With the "Add fields" button you can add additional fields to this index.
weight: 2
attributes:
data-class: button-action[data-drupal-selector="edit-add-field"]
search-api-index-fields-label:
id: search-api-index-fields-label
plugin: text
label: Label
body: A label for the field that will be used to refer to the field in most places in the user interface.
weight: 3
attributes:
data-class: details-wrapper:nth(0) table thead th:nth(0)
search-api-index-fields-machine-name:
id: search-api-index-fields-machine-name
plugin: text
label: Machine name
body: The internal ID to use for this field. Can safely be ignored by inexperienced users in most cases. Changing a field's machine name requires reindexing of the index.
weight: 4
attributes:
data-class: details-wrapper:nth(0) table thead th:nth(1)
search-api-index-fields-property-path:
id: search-api-index-fields-property-path
plugin: text
label: Property path
body: The internal relationship linking the indexed item to the field, with links being separated by colons (:). This can be useful information for advanced users, but can otherwise be ignored.
weight: 5
attributes:
data-class: details-wrapper:nth(0) table thead th:nth(2)
search-api-index-fields-type:
id: search-api-index-fields-type
plugin: text
label: Type
body: The data type to use when indexing the field. Determines how a field can be used in searches. For information on the available types, see the <a href="#search-api-data-types-table">"Data types" box</a> at the bottom of the page.
weight: 6
attributes:
data-class: details-wrapper:nth(0) table thead th:nth(3)
search-api-index-fields-boost:
id: search-api-index-fields-boost
plugin: text
label: Boost
body: Only applicable for fulltext fields. Determines how "important" the field is compared to other fulltext fields, to influence scoring of fulltext searches.
weight: 7
attributes:
data-class: details-wrapper:nth(0) table thead th:nth(4)
search-api-index-fields-edit:
id: search-api-index-fields-edit
plugin: text
label: Edit field
body: Some fields have additional configuration available, in which case an "Edit" link is displayed in the "Operations" column.
weight: 8
attributes:
data-class: details-wrapper:nth(0) table tbody td:nth(5) a
search-api-index-fields-remove:
id: search-api-index-fields-remove
plugin: text
label: Remove field
body: "Removes a field from the index again. (Note: Sometimes, a field is required (for example, by a processor) and cannot be removed.)"
weight: 9
attributes:
data-class: details-wrapper:nth(0) table tbody td:nth(6) a
search-api-index-fields-submit:
id: search-api-index-fields-submit
plugin: text
label: Save changes
body: This saves all changes made to the fields for this index. Until this button is pressed, all added, changed or removed fields will only be stored temporarily and not effect the actual index used in the rest of the site.
weight: 10
attributes:
data-id: edit-actions-submit
search-api-index-fields-cancel:
id: search-api-index-fields-cancel
plugin: text
label: Cancel changes
body: If you have made changes to the index's fields but not yet saved them, the "Cancel" link lets you discard those changes.
weight: 10
attributes:
data-id: edit-actions-cancel
@@ -0,0 +1,65 @@
id: search-api-index-form
module: search_api
label: 'Add or edit a Search API index'
langcode: en
routes:
- route_name: entity.search_api_index.add_form
- route_name: entity.search_api_index.edit_form
dependencies:
module:
- search_api
tips:
search-api-index-form-introduction:
id: search-api-index-form-introduction
plugin: text
label: Adding or editing an index
body: This form can be used to edit an existing index or add a new index to your site. Indexes define a set of data that will be indexed and can then be searched.
weight: 1
search-api-index-form-name:
id: search-api-index-form-name
plugin: text
label: Index name
body: Enter a name to identify this index. For example, "Content index". This will only be displayed in the admin user interface.
weight: 2
attributes:
data-id: edit-name
search-api-index-form-datasources:
id: search-api-index-form-datasources
plugin: text
label: Datasources
body: Datasources define the types of items that will be indexed in this index. By default, all content entities (like content, comments and taxonomy terms) will be available here, but modules can also add their own.
weight: 3
attributes:
data-id: edit-datasources
search-api-index-form-tracker:
id: search-api-index-form-tracker
plugin: text
label: Tracker
body: An index's tracker is the system that keeps track of which items there are available for the index, and which of them still need to be indexed. Changing the tracker of an existing index will lead to reindexing of all items.
weight: 4
attributes:
data-id: edit-tracker
search-api-index-form-server:
id: search-api-index-form-server
plugin: text
label: Server
body: The search server that the index should use for indexing and searching. If no server is selected here, the index cannot be enabled. An index can only have one server, but a server can have any number of indexes.
weight: 5
attributes:
data-id: edit-server
search-api-index-form-description:
id: search-api-index-form-description
plugin: text
label: Index description
body: Optionally, enter a description to explain the function of the index in more detail. This will only be displayed in the admin user interface.
weight: 6
attributes:
data-id: edit-description
search-api-index-form-options:
id: search-api-index-form-options
plugin: text
label: Advanced options
body: These options allow more detailed configuration of index behavior, but can usually safely be ignored by inexperienced users.
weight: 7
attributes:
data-id: edit-options
@@ -0,0 +1,40 @@
id: search-api-index-processors
module: search_api
label: Processors used for this index
langcode: en
routes:
- route_name: entity.search_api_index.processors
dependencies:
module:
- search_api
tips:
search-api-index-processors-introduction:
id: search-api-index-processors-introduction
plugin: text
label: Processors used for this index
body: Processors customize different aspects of an index's functionality. They can keep items from being indexed, change how certain fields are indexed and influence searches.
weight: 1
search-api-index-processors-enable:
id: search-api-index-processors-enable
plugin: text
label: Enable processors
body: "This lists all processors available for this index and lets you choose the ones that should be active. (Note: Some processors cannot be disabled.)"
weight: 2
attributes:
data-id: edit-status
search-api-index-processors-weights:
id: search-api-index-processors-weights
plugin: text
label: Processor order
body: This shows you which enabled processors will be active in the different parts of the indexing/searching workflow, and lets you re-arrange them. This should usually not be necessary, and only be used by advanced users as some processors will lead to unexpected results when used in the wrong order.
weight: 3
attributes:
data-id: edit-weights
search-api-index-processors-settings:
id: search-api-index-processors-settings
plugin: text
label: Processor settings
body: Some processors have additional configuration available, which you are able to change here.
weight: 4
attributes:
data-class: form-type-vertical-tabs
@@ -0,0 +1,104 @@
id: search-api-index
module: search_api
label: Information about an index
langcode: en
routes:
- route_name: entity.search_api_index.canonical
dependencies:
module:
- search_api
tips:
search-api-index-introduction:
id: search-api-index-introduction
plugin: text
label: Information about an index
body: This page shows a summary of a search index and its status.
weight: 1
search-api-index-index-status:
id: search-api-index-index-status
plugin: text
label: Index status
body: This gives a summary about how many items are known for this index, and how many have been indexed in their latest version. Items that are not indexed yet cannot be found by searches.
weight: 2
attributes:
data-class: search-api-index-status
search-api-index-status:
id: search-api-index-status
plugin: text
label: Status
body: Shows whether the index is currently enabled or disabled.
weight: 3
attributes:
data-class: search-api-index-summary--status
search-api-index-datasources:
id: search-api-index-datasources
plugin: text
label: Datasources
body: Lists all datasources that are enabled for this index.
weight: 4
attributes:
data-class: search-api-index-summary--datasource
search-api-index-tracker:
id: search-api-index-tracker
plugin: text
label: Tracker
body: The tracker used by the index. Only one ("Default") is available by default.
weight: 5
attributes:
data-class: search-api-index-summary--tracker
search-api-index-server:
id: search-api-index-server
plugin: text
label: Server
body: If the index is attached to a server, this server is listed here.
weight: 6
attributes:
data-class: search-api-index-summary--server
search-api-index-server-index-status:
id: search-api-index-server-index-status
plugin: text
label: Server index status
body: For enabled indexes, the number of items that can actually be retrieved from the server is listed here. For reasons why this number might differ from the number under "Index status", <a href="https://www.drupal.org/node/2009804#server-index-status">see the module's documentation</a>.
weight: 7
attributes:
data-class: search-api-index-summary--server-index-status
search-api-index-cron-batch-size:
id: search-api-index-cron-batch-size
plugin: text
label: Cron batch size
body: The number of items that will be indexed at once during cron runs.
weight: 8
attributes:
data-class: search-api-index-summary--cron-batch-size
search-api-index-index-now:
id: search-api-index-remove
plugin: text
label: Start indexing now
body: The "Start indexing now" form allows indexing items manually right away, with a batch process. Otherwise, items are only indexed during cron runs. The form might be disabled if indexing is currently not possible for some reason, or not necessary.
weight: 9
attributes:
data-id: edit-index
search-api-index-tracking:
id: search-api-index-tracking
plugin: text
label: Track items for index
body: In certain situations, the index's tracker doesn't have the latest state of the items available for indexing. This will be automatically rectified during cron runs, but can also be manually triggered here, with the "Track now" button.
weight: 10
attributes:
data-id: edit-tracking
search-api-index-reindex:
id: search-api-index-reindex
plugin: text
label: Queue all items for reindexing
body: This will queue all items on this index for reindexing. Previously indexed data will remain on the search server, so searches on this index will continue to yield results.
weight: 11
attributes:
data-id: edit-reindex
search-api-index-clear:
id: search-api-index-clear
plugin: text
label: Clear all indexed data
body: This will remove all indexed content for this index from the search server and queue it for reindexing. Searches on this index will not return any results until items are reindexed.
weight: 12
attributes:
data-id: edit-clear
@@ -0,0 +1,41 @@
id: search-api-server-form
module: search_api
label: 'Add or edit a Search API server'
langcode: en
routes:
- route_name: entity.search_api_server.add_form
- route_name: entity.search_api_server.edit_form
dependencies:
module:
- search_api
tips:
search-api-server-form-introduction:
id: search-api-server-form-introduction
plugin: text
label: Adding or editing a Server
body: This form can be used to edit an existing server or add a new server to your site. Servers will hold your indexed data.
weight: 1
search-api-server-form-name:
id: search-api-server-form-name
plugin: text
label: Server name
body: Enter a name to identify this server. For example, "Solr server". This will only be displayed in the admin user interface.
weight: 2
attributes:
data-id: edit-name
search-api-server-form-description:
id: search-api-server-form-description
plugin: text
label: Server description
body: Optionally, enter a description to explain the function of the server in more detail. This will only be displayed in the admin user interface.
weight: 3
attributes:
data-id: edit-description
search-api-server-form-backend:
id: search-api-server-form-backend
plugin: text
label: Server backend
body: Servers can be based on different technologies. These are called "backends". A server uses exactly one backend and cannot change it later. You can make the "Database" backend available by enabling the "Database Search" module. Another very common backend is <a href="https://www.drupal.org/project/search_api_solr">"Solr"</a>, which requires to be set up separately.
weight: 4
attributes:
data-id: edit-backend
@@ -0,0 +1,48 @@
id: search-api-server
module: search_api
label: Information about a server
langcode: en
routes:
- route_name: entity.search_api_server.canonical
dependencies:
module:
- search_api
tips:
search-api-server-introduction:
id: search-api-server-introduction
plugin: text
label: Information about a server
body: This page shows a summary of a search server.
weight: 1
search-api-server-status:
id: search-api-server-status
plugin: text
label: Status
body: Shows whether the server is currently enabled or disabled.
weight: 2
attributes:
data-class: search-api-server-summary--status
search-api-server-backend:
id: search-api-server-backend
plugin: text
label: Backend class
body: The backend plugin used for this server. The backend plugin determines how items are indexed and searched for example, using the database or an Apache Solr server.
weight: 3
attributes:
data-class: search-api-server-summary--backend
search-api-server-indexes:
id: search-api-server-indexes
plugin: text
label: Search indexes
body: Lists all search indexes that are attached to this server.
weight: 4
attributes:
data-class: search-api-server-summary--indexes
search-api-server-clear:
id: search-api-server-clear
plugin: text
label: Delete all indexed data
body: This will permanently remove all data currently indexed on this server for indexes that aren't read-only. Items are queued for reindexing. Until reindexing occurs, searches for the affected indexes will not return any results.
weight: 5
attributes:
data-id: edit-clear
@@ -0,0 +1,30 @@
"plugin.plugin_configuration.search_api_datasource.entity:*":
type: mapping
label: 'Entity datasource configuration'
mapping:
bundles:
type: mapping
label: 'Bundles'
mapping:
default:
type: boolean
label: 'Whether to exclude (TRUE) or include (FALSE) the selected bundles.'
selected:
type: sequence
label: 'The selected bundles'
sequence:
type: string
label: 'A bundle machine name'
languages:
type: mapping
label: 'Languages'
mapping:
default:
type: boolean
label: 'Whether to exclude (TRUE) or include (FALSE) the selected languages.'
selected:
type: sequence
label: 'The selected languages'
sequence:
type: string
label: 'A language code'
@@ -0,0 +1,95 @@
search_api.index.*:
type: config_entity
label : 'Search index'
mapping:
id:
type: string
label: 'ID'
name:
type: label
label: Name'
uuid:
type: string
label: 'UUID'
description:
type: text
label: 'Description'
read_only:
type: boolean
label: 'Read-only'
field_settings:
type: sequence
label: 'Indexed fields'
sequence:
type: mapping
label: field
mapping:
label:
type: string
label: 'A label for the field'
datasource_id:
type: string
label: 'The datasource ID of the field'
property_path:
type: string
label: 'The property path of the field'
type:
type: string
label: 'The data type of the field'
boost:
type: float
label: 'The boost of the field'
configuration:
type: search_api.property_configuration.[%parent.property_path]
indexed_locked:
type: boolean
label: 'Whether the field is locked or can be removed'
type_locked:
type: boolean
label: 'Whether the field''s data type is locked or can be changed'
hidden:
type: boolean
label: 'Whether the field should appear in the UI'
dependencies:
type: config_dependencies
label: 'The field''s dependencies'
datasource_settings:
type: sequence
label: 'Datasource settings'
sequence:
type: plugin.plugin_configuration.search_api_datasource.[%key]
label: 'The configuration for a single datasource'
processor_settings:
type: sequence
label: 'Processor settings'
sequence:
type: plugin.plugin_configuration.search_api_processor.[%key]
label: 'The configuration for a single processor'
tracker_settings:
type: sequence
label: 'Tracker settings'
sequence:
type: plugin.plugin_configuration.search_api_tracker.[%key]
label: 'The configuration for the tracker'
options:
type: mapping
label: 'Options'
mapping:
cron_limit:
type: integer
label: 'Cron batch size'
index_directly:
type: boolean
label: 'Index items immediately'
server:
type: string
label: 'Server ID'
status:
type: boolean
label: 'Status'
langcode:
type: string
label: 'Language code'
dependencies:
type: config_dependencies
label: 'Dependencies'
@@ -0,0 +1,230 @@
# Base definitions for processors
search_api.default_processor_configuration:
type: mapping
label: 'Default processor configuration'
mapping:
weights:
type: sequence
label: 'The processor''s weights for the different processing stages'
sequence:
type: integer
label: 'The processor''s weight for this stage'
search_api.fields_processor_configuration:
type: search_api.default_processor_configuration
label: 'Fields processor configuration'
mapping:
all_fields:
type: boolean
label: 'Enabled for all supported fields'
fields:
type: sequence
label: 'The selected fields'
sequence:
type: string
label: 'Selected field'
# Default for any processor without specific configuration
plugin.plugin_configuration.search_api_processor.*:
type: search_api.default_processor_configuration
# Definitions for individual processors
plugin.plugin_configuration.search_api_processor.hierarchy:
type: search_api.default_processor_configuration
label: 'Hierarchy processor configuration'
mapping:
fields:
type: sequence
label: 'Fields for which to add the hierarchy'
sequence:
type: string
label: 'Field ID'
plugin.plugin_configuration.search_api_processor.highlight:
type: search_api.default_processor_configuration
label: 'Highlight processor configuration'
mapping:
prefix:
type: string
label: 'Text/HTML that will be prepended to all occurrences of search keywords in highlighted text'
suffix:
type: string
label: 'Text/HTML that will be appended to all occurrences of search keywords in highlighted text'
excerpt:
type: boolean
label: 'When enabled, an excerpt will be created for searches with keywords, containing all occurrences of keywords in a fulltext field.'
excerpt_length:
type: integer
label: 'The requested length of the excerpt, in characters'
exclude_fields:
type: sequence
label: 'Fields excluded from excerpt'
sequence:
type: string
label: 'An excluded field''s ID'
highlight:
type: string
label: 'Defines whether returned fields should be highlighted (always/if returned/never).'
highlight_partial:
type: boolean
label: 'Whether matches in parts of words should be highlighted'
plugin.plugin_configuration.search_api_processor.html_filter:
type: search_api.fields_processor_configuration
label: 'HTML filter processor configuration'
mapping:
title:
type: boolean
label: 'Title'
alt:
type: boolean
label: 'Alt'
tags:
type: sequence
label: 'Tag boosts'
sequence:
type: integer
label: Boost
plugin.plugin_configuration.search_api_processor.ignorecase:
type: search_api.fields_processor_configuration
label: 'Ignore case processor configuration'
plugin.plugin_configuration.search_api_processor.ignore_character:
type: search_api.fields_processor_configuration
label: 'Ignore Character processor configuration'
mapping:
ignorable:
type: string
label: 'Regular expression for characters it should ignore to stem'
strip:
type: mapping
label: 'Configurable characters to ignore'
mapping:
character_sets:
type: sequence
label: 'Configuration of the character sets'
sequence:
type: ignore
label: 'Character set'
plugin.plugin_configuration.search_api_processor.stemmer:
type: search_api.fields_processor_configuration
label: 'Stemmer processor configuration'
mapping:
exceptions:
type: sequence
label: 'Stemming exceptions'
sequence:
type: string
label: Exception
plugin.plugin_configuration.search_api_processor.role_filter:
type: search_api.default_processor_configuration
label: 'Role filter processor configuration'
mapping:
default:
type: boolean
label: 'Default'
roles:
type: sequence
label: 'The selected roles'
sequence:
type: string
label: 'The role name'
plugin.plugin_configuration.search_api_processor.stopwords:
type: search_api.fields_processor_configuration
label: 'Stopwords processor configuration'
mapping:
stopwords:
type: sequence
label: 'entered stopwords'
sequence:
type: string
label: Stopword
plugin.plugin_configuration.search_api_processor.tokenizer:
type: search_api.fields_processor_configuration
label: 'Tokenizer processor configuration'
mapping:
spaces:
type: string
label: 'Regular expression for spaces'
ignorable:
type: string
label: 'Regular expression for ignorable characters'
overlap_cjk:
type: integer
label: 'Defines if simple CJK handling should be enabled.'
minimum_word_size:
type: string
label: 'Defines the minimum word size'
plugin.plugin_configuration.search_api_processor.transliteration:
type: search_api.fields_processor_configuration
label: 'Transliteration processor configuration'
plugin.plugin_configuration.search_api_processor.type_boost:
type: search_api.default_processor_configuration
label: 'Type-specific boosting processor configuration'
mapping:
boosts:
type: sequence
label: 'Boost settings'
sequence:
type: mapping
label: 'Datasource boost settings'
mapping:
datasource_boost:
type: float
label: 'Base boost for the datasource'
bundle_boosts:
type: sequence
label: 'Bundle-specific boosts'
sequence:
type: float
label: 'The boost value for this bundle'
# Definitions for property configuration
search_api.property_configuration.*:
type: mapping
label: 'Default field configuration'
mapping: {}
search_api.property_configuration.aggregated_field:
type: mapping
label: 'Aggregated field configuration'
mapping:
type:
type: string
label: 'The type of the aggregation'
fields:
type: sequence
label: 'The properties to be aggregated'
sequence:
type: string
label: 'A property that should be part of the aggregation'
search_api.property_configuration.rendered_item:
type: mapping
label: 'Rendered item processor configuration'
mapping:
roles:
type: sequence
label: 'The selected roles'
sequence:
type: string
label: 'The user roles which will be active when the entity is rendered'
view_mode:
type: sequence
label: 'The selected view modes for each datasource, by bundle'
sequence:
type: sequence
label: 'The selected view modes for the datasource, by bundle'
sequence:
type: string
label: 'The view mode used to render the entity for the specified bundle'
@@ -0,0 +1,16 @@
search_api.settings:
type: config_object
label: 'Search API settings'
mapping:
default_cron_limit:
type: integer
label: 'Default cron batch size'
cron_worker_runtime:
type: integer
label: 'Maximum runtime for the cron worker'
default_tracker:
type: string
label: 'The default tracker plugin'
tracking_page_size:
type: integer
label: 'The number of entities loaded at once when adding items to the tracker for a newly created index'
@@ -0,0 +1,33 @@
search_api.server.*:
type: config_entity
label : 'Search server'
mapping:
id:
type: string
label: 'ID'
name:
type: label
label: 'Name'
uuid:
type: string
label: 'UUID'
description:
type: text
label: 'Description'
status:
type: boolean
label: 'Status'
backend:
type: string
label: 'Backend Plugin ID'
backend_config:
type: plugin.plugin_configuration.search_api_backend.[%parent.backend]
langcode:
type: string
label: 'Language code'
dependencies:
type: config_dependencies
label: 'Dependencies'
plugin.plugin_configuration.search_api_backend.*:
type: mapping
@@ -0,0 +1,7 @@
plugin.plugin_configuration.search_api_tracker.default:
type: mapping
label: 'Entity tracker configuration'
mapping:
indexing_order:
type: string
label: 'Indexing order'
@@ -0,0 +1,294 @@
views.query.search_api_query:
type: views_query
label: 'Search API query'
mapping:
bypass_access:
type: boolean
label: If the underlying search index has access checks enabled, this option allows you to disable them for this view.
skip_access:
type: boolean
label: Do not execute additional access checks for all items in the search results.
views.row.search_api:
type: views_row
label: 'Search API rendered item'
mapping:
view_modes:
type: sequence
label: View modes for each datasource
sequence:
type: sequence
label: A list of all the bundles and their configured view mode
sequence:
type: string
label: View mode for the specific bundle
views.cache.search_api_time:
type: views_cache
label: 'Time-based caching (Search API)'
mapping:
options:
type: mapping
label: 'Cache options'
mapping:
results_lifespan:
type: integer
label: 'The length of time raw query results should be cached.'
results_lifespan_custom:
type: integer
label: 'Length of time in seconds raw query results should be cached.'
output_lifespan:
type: integer
label: 'The length of time rendered HTML output should be cached.'
output_lifespan_custom:
type: integer
label: 'Length of time in seconds rendered HTML output should be cached.'
views.cache.search_api_tag:
type: views_cache
label: 'Tag-based caching (Search API)'
mapping:
options:
type: mapping
label: 'Cache options'
views.argument.search_api:
type: views.argument.numeric
label: 'Search API'
views.argument.search_api_fulltext:
type: views.argument.search_api
label: 'Search API more like this'
mapping:
parse_mode:
type: string
label: 'Parse mode'
conjunction:
type: string
label: 'Conjunction'
fields:
type: sequence
label: 'Fields'
sequence:
type: string
label: 'Field'
views.argument.search_api_more_like_this:
type: views_argument
label: 'Search API more like this'
mapping:
fields:
type: sequence
label: 'Fields'
sequence:
type: string
label: 'Field'
views.argument.search_api_term:
type: views.argument.search_api
label: 'Search API taxonomy term'
views.field.search_api:
type: views_field
label: 'Search API standard'
mapping:
link_to_item:
type: boolean
label: 'Link to item'
use_highlighting:
type: boolean
label: 'Use highlighted field data'
multi_type:
type: string
label: 'Handling of multiple values'
multi_separator:
type: string
label: 'Separator for multiple values'
views.field.search_api_boolean:
type: views.field.boolean
label: 'Search API boolean'
mapping:
link_to_item:
type: boolean
label: 'Link to item'
use_highlighting:
type: boolean
label: 'Use highlighted field data'
multi_type:
type: string
label: 'Handling of multiple values'
multi_separator:
type: string
label: 'Separator for multiple values'
views.field.search_api_date:
type: views.field.date
label: 'Search API date'
mapping:
link_to_item:
type: boolean
label: 'Link to item'
use_highlighting:
type: boolean
label: 'Use highlighted field data'
multi_type:
type: string
label: 'Handling of multiple values'
multi_separator:
type: string
label: 'Separator for multiple values'
views.field.search_api_entity:
type: views.field.search_api
label: 'Search API entity reference'
mapping:
display_methods:
type: sequence
label: 'Display settings'
sequence:
type: mapping
label: 'Display settings for bundle'
mapping:
display_method:
type: string
label: 'Display method'
view_mode:
type: string
label: 'View mode'
views.field.search_api_field:
type: views.field.field
label: 'Search API entity field'
mapping:
field_rendering:
type: boolean
label: 'Use entity field rendering'
fallback_handler:
type: string
label: 'Fallback handler'
fallback_options:
type: views.field.[%parent.fallback_handler]
label: 'Options for fallback handler'
views.field.search_api_numeric:
type: views.field.numeric
label: 'Search API boolean'
mapping:
link_to_item:
type: boolean
label: 'Link to item'
use_highlighting:
type: boolean
label: 'Use highlighted field data'
multi_type:
type: string
label: 'Handling of multiple values'
multi_separator:
type: string
label: 'Separator for multiple values'
format_plural_values:
type: sequence
label: 'Pluralized strings'
sequence:
type: string
label: 'Singular/Plural string'
views.filter.search_api_boolean:
type: views.filter.boolean
label: 'Search API boolean'
views.filter.search_api_datasource:
type: views.filter.search_api_options
label: 'Search API datasource'
views.filter.search_api_fulltext:
type: views_filter
label: 'Search API fulltext search'
mapping:
parse_mode:
type: string
label: Parse mode
min_length:
type: integer
label: Minimum search string length
fields:
type: sequence
label: Fields to search on
sequence:
type: string
label: Field name
views.filter.search_api_language:
type: views.filter.language
label: 'Search API language'
views.filter.search_api_numeric:
type: views.filter.numeric
label: 'Search API numeric'
views.filter.search_api_options:
type: views.filter.many_to_one
label: 'Search API options'
views.filter.search_api_string:
type: views.filter.search_api_numeric
label: 'Search API string'
views.filter.search_api_term:
type: views.filter.taxonomy_index_tid
label: 'Search API taxonomy term'
views.filter.search_api_text:
type: views.filter.search_api_string
label: 'Search API text'
views.filter.search_api_user:
type: views.filter.user_name
label: 'Search API user'
views.filter_value.search_api_boolean:
type: string
views.filter_value.search_api_datasource:
type: string
views.filter_value.search_api_date:
type: views.filter_value.date
label: 'Search API date'
views.filter_value.search_api_fulltext:
type: string
views.filter_value.search_api_language:
type: string
views.filter_value.search_api_numeric:
type: views.filter_value.numeric
views.filter_value.search_api_options:
type: sequence
sequence:
type: string
label: 'Value'
views.filter_value.search_api_string:
type: views.filter_value.numeric
views.filter_value.search_api_term:
type: string
views.filter_value.search_api_text:
type: views.filter_value.numeric
views.filter_value.search_api_user:
type: string
views.relationship.search_api:
type: views_relationship
label: 'Search API'
mapping:
skip_access:
type: boolean
label: Do not execute access checks for entities referenced via this relationship.
@@ -0,0 +1,93 @@
/**
* @file
* Administration styles for the Search API module.
*/
/*
* Search API overview page
*/
.search-api-entity-list {
margin-bottom: 2em;
}
.search-api-entity-list tr {
border-bottom: none;
}
.search-api-list-server {
border-top: 1px solid #e6e4df;
}
.search-api-list-index:last-of-type {
border-bottom: 1px solid #e6e4df;
}
.search-api-list-server .search-api-type,
.search-api-list-server .search-api-title {
font-weight: bold;
}
.search-api-list-index .search-api-type {
padding-left: 3em;
}
.search-api-list-disabled .search-api-type,
.search-api-list-disabled .search-api-title {
font-style: italic;
}
/*
* Server view page
*/
.search-api-server-summary ul.inline {
margin: 0;
}
.search-api-server-summary ul.inline li {
padding-left: 0;
}
details .messages {
margin-left: 20px;
margin-right: 20px;
}
/*
* Index summary page
*/
.search-api-index-status {
margin-bottom: 6em;
}
/* Remove animation */
.search-api-index-status .progress__bar {
background-image: none;
}
.search-api-index-summary {
margin-bottom: 2em;
}
/*
* Processors page
*/
.search-api-stage-wrapper.form-item {
float: left;
box-sizing: border-box;
width: 32.66%;
min-width: 17em;
margin-right: 1%;
}
.search-api-stage-wrapper.form-item:last-child {
margin-right: 0;
}
/*
* Miscellaneous
*/
.search-api-checkboxes-list .form-checkboxes {
max-height: 30em;
overflow: auto;
}
@@ -0,0 +1,8 @@
services:
search_api.commands:
class: \Drupal\search_api\Commands\SearchApiCommands
arguments:
- '@entity_type.manager'
- '@module_handler'
tags:
- { name: drush.command }
@@ -0,0 +1,49 @@
/**
* @file
* Attaches show/hide functionality to checkboxes in the "Processor" tab.
*/
(function ($) {
'use strict';
Drupal.behaviors.searchApiProcessor = {
attach: function (context, settings) {
$('.search-api-status-wrapper input.form-checkbox', context).each(function () {
var $checkbox = $(this);
var processor_id = $checkbox.data('id');
var $rows = $('.search-api-processor-weight--' + processor_id, context);
var tab = $('.search-api-processor-settings-' + processor_id, context).data('verticalTab');
// Bind a click handler to this checkbox to conditionally show and hide
// the processor's table row and vertical tab pane.
$checkbox.on('click.searchApiUpdate', function () {
if ($checkbox.is(':checked')) {
$rows.show();
if (tab) {
tab.tabShow().updateSummary();
}
}
else {
$rows.hide();
if (tab) {
tab.tabHide().updateSummary();
}
}
});
// Attach summary for configurable items (only for screen-readers).
if (tab) {
tab.details.drupalSetSummary(function () {
return $checkbox.is(':checked') ? Drupal.t('Enabled') : Drupal.t('Disabled');
});
}
// Trigger our bound click handler to update elements to initial state.
$checkbox.triggerHandler('click.searchApiUpdate');
});
}
};
})(jQuery);
@@ -0,0 +1,60 @@
Database Search
---------------
This module provides a database-based implementation of the Search API. The
database and target to use for storing and accessing the indexes can be selected
when creating a new server.
All Search API data types are supported by using appropriate SQL data types for
their respective columns.
The "direct" parse mode for queries will result in a simple splitting of the
query string into keys. Additionally, search keys containing whitespace will be
split for all parse modes, as searching for phrases is currently not supported.
Supported optional features
---------------------------
- search_api_autocomplete
Introduced by module: search_api_autocomplete
Lets you add autocompletion capabilities to search forms on the site. (See
also "Hidden variables" below for backend-specific customization.)
NOTE: Due to internal database restrictions, this will perform significantly
better if only a single field is used for autocompletion.
- search_api_facets
Introduced by module: facets
Allows you to create faceted searches for dynamically filtering search
results.
- search_api_facets_operator_or
Introduced by module: facets
Allows the use of the "OR" operator for facets.
If you feel some backend option is missing, or have other ideas for improving
this implementation, please file a feature request in the project's issue queue,
at [1], using the "Database search" component.
[1] http://drupal.org/project/issues/search_api
Known problems
--------------
Using facets and autocomplete suggestions with a database server will only work
if the database user Drupal is using has the "CREATE TEMPORARY TABLES"
permission (or similar, in DBMSs other than MySQL).
Developer information
---------------------
Database queries for searches with this module are tagged with
"search_api_db_search" to allow easy altering. As metadata, such database
queries will have the Search API query object set as "search_api_query", and the
field settings of the server for the corresponding search index as
"search_api_db_fields".
Hidden configuration
--------------------
- search_api_db.settings.autocomplete_max_occurrences (default: 0.9)
By default, keywords that occur in more than 90% of results are ignored for
autocomplete suggestions. This setting lets you modify that behavior by
providing your own ratio. Use 1 or greater to use all suggestions.
@@ -0,0 +1 @@
autocomplete_max_occurrences: 0.9
@@ -0,0 +1,23 @@
plugin.plugin_configuration.search_api_backend.search_api_db:
type: mapping
label: 'Search API DB settings'
mapping:
database:
type: 'string'
label: 'Name of the database we are connecting to'
min_chars:
type: 'integer'
label: 'Minimum length of indexed words'
partial_matches:
type: 'boolean'
label: 'Whether to also search on parts of a word'
autocomplete:
type: mapping
label: Autcomplete configuration
mapping:
suggest_suffix:
type: boolean
label: Suggest suffix
suggest_words:
type: boolean
label: Suggest words
@@ -0,0 +1,7 @@
search_api_db.settings:
type: config_object
label: 'Search API DB settings'
mapping:
autocomplete_max_occurrences:
type: float
label: 'Maximum occurrences of a term until autocomplete suggestions ignores it'
@@ -0,0 +1,38 @@
<?php
/**
* @file
* Hooks provided by the Database Search module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Preprocess a search's database query before it is executed.
*
* This allows other modules to alter the DB query before a count query (or
* facet queries, or other related queries) are constructed from it.
*
* @param \Drupal\Core\Database\Query\SelectInterface $db_query
* The database query to be executed for the search. Will have "item_id" and
* "score" columns in its result.
* @param \Drupal\search_api\Query\QueryInterface $query
* The search query that is being executed.
*
* @see \Drupal\search_api_db\Plugin\search_api\backend\Database::preQuery()
*/
function hook_search_api_db_query_alter(\Drupal\Core\Database\Query\SelectInterface &$db_query, \Drupal\search_api\Query\QueryInterface $query) {
// If the option was set on the query, add additional SQL conditions.
if ($custom = $query->getOption('custom_sql_conditions')) {
foreach ($custom as $condition) {
$db_query->condition($condition['field'], $condition['value'], $condition['operator']);
}
}
}
/**
* @} End of "addtogroup hooks".
*/
@@ -0,0 +1,13 @@
type: module
name: Database Search
description: Offers an implementation of the Search API that uses database tables for indexing content.
package: Search
# core: 8.x
dependencies:
- search_api:search_api
# Information added by Drupal.org packaging script on 2018-02-23
version: '8.x-1.7'
core: '8.x'
project: 'search_api'
datestamp: 1519387691
@@ -0,0 +1,78 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Database Search module.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Utility\UpdateException;
/**
* Reduces the length of sort-value columns for fulltext fields to 30.
*/
function search_api_db_update_8101() {
// @see https://www.drupal.org/node/2862289
$key_value = \Drupal::keyValue('search_api_db.indexes');
foreach ($key_value->getAll() as $db_info) {
// Use the correct database from the server's backend configuration.
$database = \Drupal::config('search_api.server.' . $db_info['server'])
->get('backend_config.database');
if (!$database) {
continue;
}
list($key, $target) = explode(':', $database, 2);
$schema = Database::getConnection($target, $key)->schema();
$table = $db_info['index_table'];
foreach ($db_info['field_tables'] as $field_info) {
$column = $field_info['column'];
if ($field_info['type'] === 'text'
&& $schema->fieldExists($table, $column)) {
$spec = [
'type' => 'varchar',
'length' => 30,
'description' => "The field's value for this item",
];
$schema->changeField($table, $column, $column, $spec);
}
}
}
return t('Fulltext field database columns updated.');
}
/**
* Adds primary keys to denormalized index tables.
*/
function search_api_db_update_8102() {
// @see https://www.drupal.org/node/2884451
$key_value = \Drupal::keyValue('search_api_db.indexes');
foreach ($key_value->getAll() as $db_info) {
// Use the correct database from the server's backend configuration.
$database = \Drupal::config('search_api.server.' . $db_info['server'])
->get('backend_config.database');
if (!$database) {
continue;
}
list($key, $target) = explode(':', $database, 2);
$schema = Database::getConnection($target, $key)->schema();
$table = $db_info['index_table'];
try {
$schema->addPrimaryKey($table, ['item_id']);
}
catch (SchemaObjectExistsException $e) {
// Primary key was already added, maybe by a conscientious site admin.
// Nothing to do here in that case.
}
catch (\Exception $e) {
throw new UpdateException("Could not add a primary key to table {{$table}}: " . $e->getMessage(), 0, $e);
}
}
return t('Primary keys added to all denormalized index tables.');
}
@@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains hook implementations for the Database Search module.
*/
/**
* Implements hook_hook_info().
*/
function search_api_db_hook_info() {
return [
'search_api_db_query_alter' => [
'group' => 'search_api',
],
];
}
@@ -0,0 +1,22 @@
services:
logger.channel.search_api_db:
parent: logger.channel_base
arguments: ['search_api_db']
search_api_db.database_compatibility:
class: Drupal\search_api_db\DatabaseCompatibility\GenericDatabase
arguments: ['@database', '@transliteration']
tags:
- { name: backend_overridable }
mysql.search_api_db.database_compatibility:
class: Drupal\search_api_db\DatabaseCompatibility\MySql
arguments: ['@database', '@transliteration']
pgsql.search_api_db.database_compatibility:
class: Drupal\search_api_db\DatabaseCompatibility\CaseSensitiveDatabase
arguments: ['@database', '@transliteration']
sqlite.search_api_db.database_compatibility:
class: Drupal\search_api_db\DatabaseCompatibility\CaseSensitiveDatabase
arguments: ['@database', '@transliteration']
@@ -0,0 +1,23 @@
Database Search Defaults
------------------------
This module provides a default setup for the Search API, for searching node
content through a view, using a database server for indexing.
By installing this module on your site, the required configuration will be set
up on the site. Other than that, this module has no functionality. You can
(and should, for performance reasons) uninstall it again immediately after
installing, to just get the search set up.
Due to Drupal's configuration model, subsequent updates to the configuration
deployed with this module won't be applied to existing configuration.
The search view will be set up at this path:
/search/content
You can view (and customize) the installed search configuration under these
paths:
Server: /admin/config/search/search-api/server/default_server
Index: /admin/config/search/search-api/index/default_index
View: /admin/structure/views/view/search_content
(if the "Views UI" module is installed)
@@ -0,0 +1,43 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.search_index
- field.field.node.article.body
- field.field.node.article.comment
- field.field.node.article.field_image
- field.field.node.article.field_tags
- node.type.article
module:
- comment
- text
- user
id: node.article.search_index
targetEntityType: node
bundle: article
mode: search_index
content:
body:
type: text_default
weight: 0
settings: { }
third_party_settings: { }
label: hidden
comment:
type: comment_default
weight: 2
settings:
pager_id: 0
third_party_settings: { }
label: hidden
field_tags:
type: entity_reference_label
weight: 1
settings:
link: false
third_party_settings: { }
label: hidden
hidden:
field_image: true
langcode: true
links: true
@@ -0,0 +1,42 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.search_result
- field.field.node.article.body
- field.field.node.article.comment
- field.field.node.article.field_image
- field.field.node.article.field_tags
- node.type.article
module:
- image
- text
- user
id: node.article.search_result
targetEntityType: node
bundle: article
mode: search_result
content:
body:
type: text_summary_or_trimmed
weight: 1
settings:
trim_length: 300
third_party_settings: { }
label: hidden
field_image:
type: image
weight: 0
settings:
image_style: thumbnail
image_link: content
third_party_settings: { }
label: hidden
links:
weight: 2
settings: { }
third_party_settings: { }
hidden:
comment: true
field_tags: true
langcode: true
@@ -0,0 +1,24 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.search_index
- field.field.node.page.body
- node.type.page
module:
- text
- user
id: node.page.search_index
targetEntityType: node
bundle: page
mode: search_index
content:
body:
type: text_default
weight: 0
settings: { }
third_party_settings: { }
label: hidden
hidden:
langcode: true
links: true
@@ -0,0 +1,28 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.search_result
- field.field.node.page.body
- node.type.page
module:
- text
- user
id: node.page.search_result
targetEntityType: node
bundle: page
mode: search_result
content:
body:
type: text_summary_or_trimmed
weight: 100
settings:
trim_length: 300
third_party_settings: { }
label: hidden
links:
weight: 101
settings: { }
third_party_settings: { }
hidden:
langcode: true
@@ -0,0 +1,188 @@
id: default_index
name: 'Default content index'
description: 'Default content index created by the Database Search Defaults module'
read_only: false
field_settings:
title:
label: Title
type: text
datasource_id: 'entity:node'
property_path: title
boost: 8
rendered_item:
label: 'Rendered item'
type: text
property_path: rendered_item
configuration:
roles:
anonymous: anonymous
view_mode:
'entity:node':
article: search_index
page: search_index
created:
label: 'Authored on'
type: date
datasource_id: 'entity:node'
property_path: created
changed:
label: Changed
type: date
datasource_id: 'entity:node'
property_path: changed
status:
label: 'Publishing status'
type: boolean
datasource_id: 'entity:node'
property_path: status
index_locked: true
type_locked: true
sticky:
label: 'Sticky at top of lists'
type: boolean
datasource_id: 'entity:node'
property_path: sticky
field_tags:
label: Tags
type: integer
datasource_id: 'entity:node'
property_path: 'field_tags'
author:
label: 'Author name'
type: string
datasource_id: 'entity:node'
property_path: 'uid:entity:name'
uid:
label: 'Author ID'
type: integer
datasource_id: 'entity:node'
property_path: uid
index_locked: true
type_locked: true
node_grants:
label: 'Node access information'
type: string
property_path: search_api_node_grants
index_locked: true
type_locked: true
hidden: true
type:
label: 'Content type'
type: string
datasource_id: 'entity:node'
property_path: type
processor_settings:
add_url:
weights:
preprocess_index: -30
aggregated_field:
weights:
add_properties: 20
content_access:
weights:
preprocess_index: -6
preprocess_query: -4
html_filter:
weights:
preprocess_index: -3
preprocess_query: -6
fields:
- rendered_item
title: true
alt: true
tags:
h1: 5
h2: 3
h3: 2
string: 2
b: 2
ignorecase:
weights:
preprocess_index: -5
preprocess_query: -8
fields:
- rendered_item
- title
entity_status:
weights:
preprocess_index: -10
rendered_item:
weights:
add_properties: 0
pre_index_save: -10
stopwords:
weights:
preprocess_query: -10
postprocess_query: -10
fields:
- rendered_item
- title
stopwords:
- a
- an
- and
- are
- as
- at
- be
- but
- by
- for
- if
- in
- into
- is
- it
- 'no'
- not
- of
- 'on'
- or
- s
- such
- t
- that
- the
- their
- then
- there
- these
- they
- this
- to
- was
- will
- with
tokenizer:
weights:
preprocess_index: -2
preprocess_query: -5
fields:
- rendered_item
- title
spaces: ''
overlap_cjk: 1
minimum_word_size: '3'
transliteration:
weights:
preprocess_index: -4
preprocess_query: -7
fields:
- rendered_item
- title
options:
index_directly: true
cron_limit: 50
datasource_settings:
'entity:node': { }
tracker_settings:
'default': { }
server: default_server
status: true
langcode: en
dependencies:
config:
- field.field.node.article.field_tags
- search_api.server.default_server
module:
- node
@@ -0,0 +1,16 @@
id: default_server
name: 'Database Server'
description: 'Default database server created by the Database Search Defaults module'
status: true
backend: search_api_db
backend_config:
database: 'default:default'
min_chars: 3
partial_matches: false
autocomplete:
suggest_suffix: true
suggest_words: true
langcode: en
dependencies:
module:
- search_api_db
@@ -0,0 +1,159 @@
id: search_content
langcode: en
status: true
dependencies:
module:
- search_api
config:
- core.entity_view_mode.node.search_result
- core.entity_view_display.node.article.search_result
- core.entity_view_display.node.page.search_result
- search_api.index.default_index
label: 'Search content'
module: views
description: 'A search page preconfigured to search through the content of your site'
tag: ''
base_table: search_api_index_default_index
base_field: search_api_id
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: none
options: { }
query:
type: search_api_query
options: { }
exposed_form:
type: input_required
options:
submit_button: Search
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
text_input_required: 'Please enter some keywords to search.'
text_input_required_format: basic_html
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
tags:
previous: ' previous'
next: 'next '
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
style:
type: default
row:
type: search_api
options:
view_modes:
'entity:node':
article: search_result
page: search_result
filters:
search_api_fulltext:
id: search_api_fulltext
table: search_api_index_default_index
field: search_api_fulltext
relationship: none
group_type: group
admin_label: ''
operator: and
value: ''
group: 1
exposed: true
expose:
operator_id: search_op
label: Search
description: ''
use_operator: false
operator: search_op
identifier: keys
required: true
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
parse_mode: terms
min_length: 3
fields: { }
plugin_id: search_api_fulltext
sorts: { }
title: 'Search Content'
header:
result:
id: result
table: views
field: result
relationship: none
group_type: group
admin_label: ''
empty: false
content: 'Displaying results @start - @end of @total'
plugin_id: result
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
cacheable: false
max-age: 0
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: search/content
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
cacheable: false
max-age: 0
tags: { }
@@ -0,0 +1,20 @@
type: module
name: Database Search Defaults
description: Enable this module for a best-practice default setup of Search API with the Database backend. After installation it is recommended to uninstall this module again for performance reasons. The provided configuration will not be removed.
package: Search
# core: 8.x
dependencies:
- drupal:comment
- drupal:field
- drupal:image
- drupal:node
- drupal:text
- drupal:user
- drupal:views
- search_api:search_api_db
# Information added by Drupal.org packaging script on 2018-02-23
version: '8.x-1.7'
core: '8.x'
project: 'search_api'
datestamp: 1519387691
@@ -0,0 +1,85 @@
<?php
/**
* @file
* Install, update and uninstall functions for the DB Search Defaults module.
*/
use Drupal\node\Entity\NodeType;
/**
* Implements hook_requirements().
*/
function search_api_db_defaults_requirements($phase) {
$requirements = [];
if ($phase == 'install') {
$node_types = NodeType::loadMultiple();
$required_types = [
'article' => ['body', 'comment', 'field_tags', 'field_image'],
'page' => ['body'],
];
/** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */
$entity_field_manager = \Drupal::service('entity_field.manager');
foreach ($required_types as $required_type_id => $required_fields) {
if (!isset($node_types[$required_type_id])) {
$requirements['search_api_db_defaults:' . $required_type_id] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => $required_type_id]),
];
}
else {
// Check if all the fields are here.
$fields = $entity_field_manager->getFieldDefinitions('node', $required_type_id);
foreach ($required_fields as $required_field) {
if (!isset($fields[$required_field])) {
$requirements['search_api_db_defaults:' . $required_type_id . ':' . $required_field] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('Field @field in content type @node_type not found. Database Search Defaults module could not be installed', ['@node_type' => $required_type_id, '@field' => $required_field]),
];
}
}
}
}
if (\Drupal::moduleHandler()->moduleExists('search_api_db')) {
$entities_to_check = [
'search_api_index' => 'default_index',
'search_api_server' => 'default_server',
'view' => 'search_content',
];
/** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
foreach ($entities_to_check as $entity_type => $entity_id) {
// Find out if the entity is already in place. If so, fail to install the
// module.
$entity_storage = $entity_type_manager->getStorage($entity_type);
$entity_storage->resetCache();
$entity = $entity_storage->load($entity_id);
if ($entity) {
$requirements['search_api_db_defaults:defaults_exist'] = [
'severity' => REQUIREMENT_ERROR,
'description' => t('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.'),
];
break;
}
}
}
}
return $requirements;
}
/**
* Implements hook_install().
*/
function search_api_db_defaults_install() {
// Clear the display plugin cache after installation so the plugin for the new
// view (display) gets found.
\Drupal::service('plugin.manager.search_api.display')
->clearCachedDefinitions();
}
@@ -0,0 +1,181 @@
<?php
namespace Drupal\Tests\search_api_db_defaults\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Entity\Server;
use Drupal\Tests\search_api\Functional\SearchApiBrowserTestBase;
/**
* Tests the correct installation of the default configs.
*
* @group search_api
*/
class IntegrationTest extends SearchApiBrowserTestBase {
use StringTranslationTrait, CommentTestTrait, EntityReferenceTestTrait;
/**
* The profile to install as a basis for testing.
*
* @var string
*/
protected $profile = 'standard';
/**
* A non-admin user used for this test.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $authenticatedUser;
/**
* An admin user used for this test.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create user with content access permission to see if the view is
// accessible, and an admin to do the setup.
$this->authenticatedUser = $this->drupalCreateUser();
$this->adminUser = $this->drupalCreateUser([], NULL, TRUE);
}
/**
* Tests whether the default search was correctly installed.
*/
public function testInstallAndDefaultSetupWorking() {
$this->drupalLogin($this->adminUser);
// Installation invokes a batch and this breaks it.
\Drupal::state()->set('search_api_use_tracking_batch', FALSE);
// Install the search_api_db_defaults module.
$edit_enable = [
'modules[search_api_db_defaults][enable]' => TRUE,
];
$this->drupalPostForm('admin/modules', $edit_enable, 'Install');
$this->assertSession()->pageTextContains('Some required modules must be enabled');
$this->drupalPostForm(NULL, [], 'Continue');
$this->assertSession()->pageTextContains('2 modules have been enabled: Database Search Defaults, Database Search');
$this->rebuildContainer();
$this->drupalPostForm('admin/config/search/search-api/server/default_server/edit', [], 'Save');
$this->assertSession()->pageTextContains('The server was successfully saved.');
$server = Server::load('default_server');
$this->assertTrue($server, 'Server can be loaded');
$index = Index::load('default_index');
$this->assertTrue($index, 'Index can be loaded');
$this->drupalLogin($this->authenticatedUser);
$this->drupalGet('search/content');
$this->assertSession()->statusCodeEquals(200);
$this->drupalLogin($this->adminUser);
$title = 'Test node title';
$edit = [
'title[0][value]' => $title,
'body[0][value]' => 'This is test content for the Search API to index.',
];
$this->drupalPostForm('node/add/article', $edit, 'Save');
$this->drupalLogout();
$this->drupalGet('search/content');
$this->assertSession()->pageTextContains('Please enter some keywords to search.');
$this->assertSession()->pageTextNotContains($title);
$this->assertSession()->responseNotContains('Error message');
$this->submitForm([], 'Search');
$this->assertSession()->pageTextNotContains($title);
$this->assertSession()->responseNotContains('Error message');
$this->submitForm(['keys' => 'test'], 'Search');
$this->assertSession()->pageTextContains($title);
$this->assertSession()->responseNotContains('Error message');
// Uninstall the module.
$this->drupalLogin($this->adminUser);
$edit_disable = [
'uninstall[search_api_db_defaults]' => TRUE,
];
$this->drupalPostForm('admin/modules/uninstall', $edit_disable, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->rebuildContainer();
$this->assertFalse($this->container->get('module_handler')->moduleExists('search_api_db_defaults'), 'Search API DB Defaults module uninstalled.');
// Check if the server is found in the Search API admin UI.
$this->drupalGet('admin/config/search/search-api/server/default_server');
$this->assertSession()->statusCodeEquals(200);
// Check if the index is found in the Search API admin UI.
$this->drupalGet('admin/config/search/search-api/index/default_index');
$this->assertSession()->statusCodeEquals(200);
// Check that saving any of the index's config forms works fine.
foreach (['edit', 'fields', 'processors'] as $tab) {
$submit = $tab == 'fields' ? 'Save changes' : 'Save';
$this->drupalGet("admin/config/search/search-api/index/default_index/$tab");
$this->submitForm([], $submit);
$this->assertSession()->statusCodeEquals(200);
}
$this->drupalLogin($this->authenticatedUser);
$this->drupalGet('search/content');
$this->assertSession()->statusCodeEquals(200);
$this->drupalLogin($this->adminUser);
// Enable the module again. This should fail because the either the index
// or the server or the view was found.
$this->drupalPostForm('admin/modules', $edit_enable, 'Install');
$this->assertSession()->pageTextContains('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.');
// Delete all the entities that we would fail on if they exist.
$entities_to_remove = [
'search_api_index' => 'default_index',
'search_api_server' => 'default_server',
'view' => 'search_content',
];
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
foreach ($entities_to_remove as $entity_type => $entity_id) {
/** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */
$entity_storage = $entity_type_manager->getStorage($entity_type);
$entity_storage->resetCache();
$entities = $entity_storage->loadByProperties(['id' => $entity_id]);
if (!empty($entities[$entity_id])) {
$entities[$entity_id]->delete();
}
}
// Delete the article content type.
$this->drupalGet('node/1/delete');
$this->submitForm([], 'Delete');
$this->drupalGet('admin/structure/types/manage/article');
$this->clickLink('Delete');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Delete');
// Try to install search_api_db_defaults module and test if it failed
// because there was no content type "article".
$this->drupalPostForm('admin/modules', $edit_enable, 'Install');
$success_text = new FormattableMarkup('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => 'article']);
$this->assertSession()->pageTextContains($success_text);
}
}
@@ -0,0 +1,17 @@
<?php
namespace Drupal\search_api_db\DatabaseCompatibility;
/**
* Represents a database whose tables are, by default, case-sensitive.
*/
class CaseSensitiveDatabase extends GenericDatabase {
/**
* {@inheritdoc}
*/
public function preprocessIndexValue($value, $type = 'text') {
return $value;
}
}
@@ -0,0 +1,73 @@
<?php
namespace Drupal\search_api_db\DatabaseCompatibility;
use Drupal\Core\Database\Connection;
/**
* Bundles methods for resolving DBMS-specific differences.
*
* @internal This interface and all implementing classes are just used by the
* search_api_db module for internal purposes. They should not be relied upon
* in other modules.
*/
interface DatabaseCompatibilityHandlerInterface {
/**
* Retrieves the database connection this compatibility handler is based upon.
*
* @return \Drupal\Core\Database\Connection
* The database connection.
*/
public function getDatabase();
/**
* Creates a clone of this service for the given database.
*
* @param \Drupal\Core\Database\Connection $database
* A database of a type compatible with this class.
*
* @return static
* A clone of this service class for the given database.
*/
public function getCloneForDatabase(Connection $database);
/**
* Reacts to a new table being created.
*
* @param string $table
* The name of the table.
* @param string $type
* (optional) The type of table. One of "index" (for the denormalized table
* for an entire index), "text" (for an index's fulltext data table) and
* "field" (for field-specific tables).
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if any error occurs that should abort the current action. Internal
* errors that can be ignored should just be logged.
*/
public function alterNewTable($table, $type = 'text');
/**
* Determines the canonical base form of a value.
*
* For example, when the table is case-insensitive, the value should always be
* lowercased (or always uppercased) to arrive at the canonical base form.
*
* If tables of the given type use binary comparison in this database, the
* value should not be changed.
*
* @param string $value
* A string to be indexed or searched for.
* @param string $type
* (optional) The type of table. One of "index" (for the denormalized table
* for an entire index), "text" (for an index's fulltext data table) and
* "field" (for field-specific tables).
*
* @return string
* The value in its canonical base form, which won't clash with any other
* canonical base form when inserted into a table of the given type.
*/
public function preprocessIndexValue($value, $type = 'text');
}
@@ -0,0 +1,72 @@
<?php
namespace Drupal\search_api_db\DatabaseCompatibility;
use Drupal\Component\Transliteration\TransliterationInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\Connection;
/**
* Represents any database for which no specifics are known.
*/
class GenericDatabase implements DatabaseCompatibilityHandlerInterface {
/**
* The connection to the database.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The transliteration service to use.
*
* @var \Drupal\Component\Transliteration\TransliterationInterface
*/
protected $transliterator;
/**
* Constructs a GenericDatabase object.
*
* @param \Drupal\Core\Database\Connection $database
* The connection to the database.
* @param \Drupal\Component\Transliteration\TransliterationInterface $transliterator
* The transliteration service to use.
*/
public function __construct(Connection $database, TransliterationInterface $transliterator) {
$this->database = $database;
$this->transliterator = $transliterator;
}
/**
* {@inheritdoc}
*/
public function getDatabase() {
return $this->database;
}
/**
* {@inheritdoc}
*/
public function getCloneForDatabase(Connection $database) {
$service = clone $this;
$service->database = $database;
return $service;
}
/**
* {@inheritdoc}
*/
public function alterNewTable($table, $type = 'text') {}
/**
* {@inheritdoc}
*/
public function preprocessIndexValue($value, $type = 'text') {
if ($type == 'text') {
return $value;
}
return Unicode::strtolower($this->transliterator->transliterate($value));
}
}
@@ -0,0 +1,39 @@
<?php
namespace Drupal\search_api_db\DatabaseCompatibility;
use Drupal\Core\Database\DatabaseException;
use Drupal\search_api\SearchApiException;
/**
* Represents a MySQL-based database.
*/
class MySql extends GenericDatabase {
/**
* {@inheritdoc}
*/
public function alterNewTable($table, $type = 'text') {
// The Drupal MySQL integration defaults to using a 4-byte-per-character
// encoding, which would make it impossible to use our normal 255 characters
// long varchar fields in a primary key (since that would exceed the key's
// maximum size). Therefore, we have to convert all tables to the "utf8"
// character set but we only want to make fulltext tables case-sensitive.
$charset = $type === 'text' ? 'utf8mb4' : 'utf8';
$collation = $type === 'text' ? 'utf8mb4_bin' : 'utf8_general_ci';
try {
$this->database->query("ALTER TABLE {{$table}} CONVERT TO CHARACTER SET '$charset' COLLATE '$collation'");
}
catch (\PDOException $e) {
$class = get_class($e);
$message = $e->getMessage();
throw new SearchApiException("$class while trying to change collation of $type search data table '$table': $message", 0, $e);
}
catch (DatabaseException $e) {
$class = get_class($e);
$message = $e->getMessage();
throw new SearchApiException("$class while trying to change collation of $type search data table '$table': $message", 0, $e);
}
}
}
@@ -0,0 +1,67 @@
<?php
namespace Drupal\search_api_db\Tests;
use Drupal\Core\Database\SchemaObjectExistsException;
/**
* Provides some common helper methods for database tests.
*/
trait DatabaseTestsTrait {
/**
* Asserts that the given table exists and has a primary key.
*
* @param string $table
* The name of the table.
* @param string|null $message
* (optional) The message to print for the assertion, or NULL to use an
* automatically generated one.
*/
protected function assertHasPrimaryKey($table, $message = NULL) {
$schema = \Drupal::database()->schema();
$this->assertTrue($schema->tableExists($table), "Table $table exists.");
if (!$message) {
$message = "Table $table has a primary key.";
}
// The database layer doesn't support generic introspection into primary
// keys. The simplest way to test whether a primary key exists is therefore
// to try to create one and see whether that leads to an exception.
try {
$schema->addPrimaryKey($table, []);
$this->assertTrue(FALSE, $message);
}
catch (SchemaObjectExistsException $e) {
$this->assertTrue(TRUE, $message);
}
catch (\Exception $e) {
// Trying to create a primary key with an empty fields list will probably
// still throw an exception, so we catch that as well.
$this->assertTrue(FALSE, $message);
}
}
/**
* Asserts that the given table exists and does not have a primary key.
*
* @param string $table
* The name of the table.
* @param string|null $message
* (optional) The message to print for the assertion, or NULL to use an
* automatically generated one.
*/
protected function assertNotHasPrimaryKey($table, $message = NULL) {
$schema = \Drupal::database()->schema();
$this->assertTrue($schema->tableExists($table), "Table $table exists.");
if (!$message) {
$message = "Table $table does not have a primary key.";
}
// The database layer doesn't support generic introspection into primary
// keys. The simplest way to make sure a table does not have a primary key
// is trying to drop it and checking the return value.
$this->assertFalse($schema->dropPrimaryKey($table), $message);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Drupal\search_api_db\Tests\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\search_api_db\Tests\DatabaseTestsTrait;
/**
* Tests whether search_api_db_update_8102() works correctly.
*
* @group search_api
*
* @see https://www.drupal.org/node/2884451
*/
class SearchApiDbUpdate8102Test extends UpdatePathTestBase {
use DatabaseTestsTrait;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// We need to manually set our entity types as "installed".
$entity_type_ids = [
'search_api_index',
'search_api_server',
'search_api_task'
];
foreach ($entity_type_ids as $entity_type_id) {
$entity_type = \Drupal::getContainer()
->get('entity_type.manager')
->getDefinition($entity_type_id);
\Drupal::getContainer()
->get('entity_type.listener')
->onEntityTypeCreate($entity_type);
}
}
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
DRUPAL_ROOT . '/core/modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/search-api-db-base.php',
__DIR__ . '/../../../tests/fixtures/update/search-api-db-update-8102.php',
];
}
/**
* Tests whether search_api_db_update_8102() works correctly.
*
* @see https://www.drupal.org/node/2884451
*/
public function testUpdate8102() {
$this->assertNotHasPrimaryKey('search_api_db_index_1');
$this->assertHasPrimaryKey('search_api_db_index_2');
$this->runUpdates();
$this->assertHasPrimaryKey('search_api_db_index_1');
$this->assertHasPrimaryKey('search_api_db_index_2');
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz.
*
* Can be used for setting up a base Search API DB installation.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
// Set the schema versions.
$versions = [
'search_api' => 8104,
'search_api_db' => 8101,
];
foreach ($versions as $name => $version) {
$connection->insert('key_value')
->fields([
'collection' => 'system.schema',
'name' => $name,
'value' => serialize($version),
])
->execute();
}
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['search_api'] = 0;
$extensions['module']['search_api_db'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Install the default configuration.
$configs['search_api.settings'] = Yaml::decode(file_get_contents(__DIR__ . '/../../../../../config/install/search_api.settings.yml'));
$configs['search_api_db.settings'] = Yaml::decode(file_get_contents(__DIR__ . '/../../../config/install/search_api_db.settings.yml'));
foreach ($configs as $name => $config) {
$data = $connection->insert('config')
->fields([
'name' => $name,
'data' => serialize($config),
'collection' => '',
])
->execute();
}
@@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz.
*
* Used for testing the search_api_db_update_8102() update.
*
* @see \Drupal\search_api_db\Tests\Update\SearchApiDbUpdate8102Test
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
// The update hook needs the server config, though only the "database" config
// setting is actually relevant.
$server_configs[] = Yaml::decode(file_get_contents(__DIR__ . '/../../../search_api_db_defaults/config/optional/search_api.server.default_server.yml'));
foreach ($server_configs as $server_config) {
$connection->insert('config')
->fields([
'collection' => '',
'name' => 'search_api.server.' . $server_config['id'],
'data' => serialize($server_config),
])
->execute();
}
foreach ([1, 2] as $i) {
$name = "index_$i";
$table = "search_api_db_$name";
$value = [
'server' => 'default_server',
'index_table' => $table,
];
$connection->insert('key_value')
->fields([
'collection' => 'search_api_db.indexes',
'name' => $name,
'value' => serialize($value),
])
->execute();
$definition = [
'name' => $table,
'module' => 'search_api_db',
'fields' => [
'item_id' => [
'type' => 'varchar',
'length' => 150,
'description' => 'The primary identifier of the item',
'not null' => TRUE,
],
],
];
if ($i === 2) {
$definition['primary key'] = ['item_id'];
}
$connection->schema()->createTable($table, $definition);
}
@@ -0,0 +1,15 @@
id: search_api_db_test_autocomplete
langcode: en
status: true
dependencies:
config:
- search_api.index.database_search_index
label: Autocomplete test module search
index_id: database_search_index
suggester_settings:
server:
fields: { }
search_settings:
'search_api_db_test_autocomplete': { }
options:
show_count: true
@@ -0,0 +1,15 @@
name: 'Search API Database Autocomplete Test'
type: module
description: 'Support module for testing Autocomplete support of the "Database" backend'
package: Search
dependencies:
- search_api:search_api_test_views
- search_api_autocomplete:search_api_autocomplete
# core: 8.x
hidden: true
# Information added by Drupal.org packaging script on 2018-02-23
version: '8.x-1.7'
core: '8.x'
project: 'search_api'
datestamp: 1519387691
@@ -0,0 +1,70 @@
<?php
namespace Drupal\search_api_db_test_autocomplete\Plugin\search_api_autocomplete\search;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search_api_autocomplete\Search\SearchPluginBase;
use Drupal\search_api_test\TestPluginTrait;
/**
* Defines a test type class.
*
* @SearchApiAutocompleteSearch(
* id = "search_api_db_test_autocomplete",
* label = @Translation("Autocomplete test module search"),
* description = @Translation("Test autocomplete search"),
* group_label = @Translation("Test search"),
* group_description = @Translation("Searches used for tests"),
* index = "database_search_index",
* )
*/
class TestSearch extends SearchPluginBase implements PluginFormInterface {
use TestPluginTrait;
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$this->logMethodCall(__FUNCTION__, func_get_args());
if ($override = $this->getMethodOverride(__FUNCTION__)) {
return call_user_func($override, $this, $form, $form_state);
}
return [];
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->logMethodCall(__FUNCTION__, func_get_args());
if ($override = $this->getMethodOverride(__FUNCTION__)) {
call_user_func($override, $this, $form, $form_state);
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->logMethodCall(__FUNCTION__, func_get_args());
if ($override = $this->getMethodOverride(__FUNCTION__)) {
call_user_func($override, $this, $form, $form_state);
return;
}
$this->setConfiguration($form_state->getValues());
}
/**
* {@inheritdoc}
*/
public function createQuery($keys, array $data = []) {
$this->logMethodCall(__FUNCTION__, func_get_args());
if ($override = $this->getMethodOverride(__FUNCTION__)) {
return call_user_func($override, $this, $keys, $data);
}
return $this->search->getIndex()->query()->keys($keys);
}
}
@@ -0,0 +1,40 @@
<?php
namespace Drupal\Tests\search_api_db\FunctionalJavascript;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests that using the DB backend via the UI works as expected.
*
* @group search_api
*/
class IntegrationTest extends JavascriptTestBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'search_api',
'search_api_db',
];
/**
* Tests that adding a server works.
*/
public function testAddingServer() {
$admin_user = $this->drupalCreateUser(['administer search_api', 'access administration pages']);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/search/search-api/add-server');
$this->assertSession()->statusCodeEquals(200);
$edit = ['name' => ' ~`Test Server', 'id' => '_test'];
$this->submitForm($edit, 'Save');
$this->assertSession()->addressEquals('admin/config/search/search-api/server/_test');
}
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\Tests\search_api_db\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\search_api\Utility\Utility;
use Drupal\search_api_autocomplete\Entity\Search;
use Drupal\search_api_db\Plugin\search_api\backend\Database;
use Drupal\Tests\search_api\Functional\ExampleContentTrait;
/**
* Tests autocomplete functionality of the Database backend.
*
* @requires module search_api_autocomplete
* @coversDefaultClass \Drupal\search_api_db\Plugin\search_api\backend\Database
* @group search_api
*/
class AutocompleteTest extends KernelTestBase {
use ExampleContentTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'entity_test',
'field',
'system',
'text',
'user',
'search_api',
'search_api_autocomplete',
'search_api_db',
'search_api_db_test_autocomplete',
'search_api_test_db',
'search_api_test_example_content',
];
/**
* A search server ID.
*
* @var string
*/
protected $serverId = 'database_search_server';
/**
* A search index ID.
*
* @var string
*/
protected $indexId = 'database_search_index';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installSchema('search_api', ['search_api_item']);
$this->installSchema('system', ['router']);
$this->installSchema('user', ['users_data']);
$this->installEntitySchema('entity_test_mulrev_changed');
$this->installEntitySchema('search_api_task');
$this->installConfig('search_api');
// Do not use a batch for tracking the initial items after creating an
// index when running the tests via the GUI. Otherwise, it seems Drupal's
// Batch API gets confused and the test fails.
if (!Utility::isRunningInCli()) {
\Drupal::state()->set('search_api_use_tracking_batch', FALSE);
}
$this->installConfig([
'search_api_db',
'search_api_test_example_content',
'search_api_test_db',
'search_api_db_test_autocomplete',
]);
$this->setUpExampleStructure();
$this->insertExampleContent();
$this->indexItems($this->indexId);
}
/**
* Tests whether autocomplete suggestions are correctly created.
*
* @covers ::getAutocompleteSuggestions
*/
public function testAutocompletion() {
/** @var \Drupal\search_api_autocomplete\SearchInterface $autocomplete */
$autocomplete = Search::load('search_api_db_test_autocomplete');
$index = $autocomplete->getIndex();
/** @var \Drupal\search_api_db\Plugin\search_api\backend\Database $backend */
$backend = $index->getServerInstance()->getBackend();
$this->assertInstanceOf(Database::class, $backend);
$query = $index->query()
->range(0, 10);
$suggestions = $backend->getAutocompleteSuggestions($query, $autocomplete, 'fo', 'fo');
$expected = [
'foo' => 4,
'foobar' => 1,
'foobaz' => 1,
'foobuz' => 1,
];
$this->assertSuggestionsEqual($expected, $suggestions);
$query = $index->query()
->keys('foo')
->range(0, 10);
$suggestions = $backend->getAutocompleteSuggestions($query, $autocomplete, 'fo', 'foo fo');
$expected = [
'foo foobaz' => 1,
'foo foobuz' => 1,
];
$this->assertSuggestionsEqual($expected, $suggestions);
}
/**
* Asserts that the returned suggestions are as expected.
*
* @param int[] $expected
* Associative array mapping suggestion strings to their counts.
* @param \Drupal\search_api_autocomplete\Suggestion\SuggestionInterface[] $suggestions
* The suggestions returned by the backend.
*/
protected function assertSuggestionsEqual(array $expected, array $suggestions) {
$terms = [];
foreach ($suggestions as $suggestion) {
$keys = $suggestion->getSuggestedKeys();
if ($keys === NULL) {
$keys = $suggestion->getSuggestionPrefix();
$keys .= $suggestion->getUserInput();
$keys .= $suggestion->getSuggestionSuffix();
}
$terms[$keys] = $suggestion->getResultsCount();
}
$this->assertEquals($expected, $terms);
}
}
@@ -0,0 +1,753 @@
<?php
namespace Drupal\Tests\search_api_db\Kernel;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Database as CoreDatabase;
use Drupal\search_api\Entity\Server;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
use Drupal\search_api\Plugin\search_api\data_type\value\TextValue;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\Utility;
use Drupal\search_api_db\DatabaseCompatibility\GenericDatabase;
use Drupal\search_api_db\Plugin\search_api\backend\Database;
use Drupal\search_api_db\Tests\DatabaseTestsTrait;
use Drupal\Tests\search_api\Kernel\BackendTestBase;
/**
* Tests index and search capabilities using the Database search backend.
*
* @see \Drupal\search_api_db\Plugin\search_api\backend\Database
*
* @group search_api
*/
class BackendTest extends BackendTestBase {
use DatabaseTestsTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'search_api_db',
'search_api_test_db',
];
/**
* {@inheritdoc}
*/
protected $serverId = 'database_search_server';
/**
* {@inheritdoc}
*/
protected $indexId = 'database_search_index';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create a dummy table that will cause a naming conflict with the backend's
// default table names, thus testing whether it correctly reacts to such
// conflicts.
\Drupal::database()->schema()->createTable('search_api_db_database_search_index', [
'fields' => [
'id' => [
'type' => 'int',
],
],
]);
$this->installConfig(['search_api_test_db']);
}
/**
* {@inheritdoc}
*/
protected function checkBackendSpecificFeatures() {
$this->checkMultiValuedInfo();
$this->editServerPartial();
$this->searchSuccessPartial();
$this->editServerMinChars();
$this->searchSuccessMinChars();
$this->checkUnknownOperator();
$this->checkDbQueryAlter();
$this->checkFieldIdChanges();
}
/**
* {@inheritdoc}
*/
protected function backendSpecificRegressionTests() {
$this->regressionTest2557291();
$this->regressionTest2511860();
$this->regressionTest2846932();
$this->regressionTest2926733();
$this->regressionTest2938646();
}
/**
* Tests that all tables and all columns have been created.
*/
protected function checkServerBackend() {
$db_info = $this->getIndexDbInfo();
$normalized_storage_table = $db_info['index_table'];
$field_infos = $db_info['field_tables'];
$expected_fields = [
'body',
'category',
'created',
'id',
'keywords',
'name',
'search_api_datasource',
'search_api_language',
'type',
'width',
];
$actual_fields = array_keys($field_infos);
sort($actual_fields);
$this->assertEquals($expected_fields, $actual_fields, 'All expected field tables were created.');
$this->assertTrue(\Drupal::database()->schema()->tableExists($normalized_storage_table), 'Normalized storage table exists.');
$this->assertHasPrimaryKey($normalized_storage_table, 'Normalized storage table has a primary key.');
foreach ($field_infos as $field_id => $field_info) {
if ($field_id != 'search_api_id') {
$this->assertTrue(\Drupal::database()
->schema()
->tableExists($field_info['table']));
}
else {
$this->assertEmpty($field_info['table']);
}
$this->assertTrue(\Drupal::database()->schema()->fieldExists($normalized_storage_table, $field_info['column']), new FormattableMarkup('Field column %column exists', ['%column' => $field_info['column']]));
}
}
/**
* Checks whether changes to the index's fields are picked up by the server.
*/
protected function updateIndex() {
/** @var \Drupal\search_api\IndexInterface $index */
$index = $this->getIndex();
// Remove a field from the index and check if the change is matched in the
// server configuration.
$field = $index->getField('keywords');
if (!$field) {
throw new \Exception();
}
$index->removeField('keywords');
$index->save();
$index_fields = array_keys($index->getFields());
// Include the three "magic" fields we're indexing with the DB backend.
$index_fields[] = 'search_api_datasource';
$index_fields[] = 'search_api_language';
$db_info = $this->getIndexDbInfo();
$server_fields = array_keys($db_info['field_tables']);
sort($index_fields);
sort($server_fields);
$this->assertEquals($index_fields, $server_fields);
// Add the field back for the next assertions.
$index->addField($field)->save();
}
/**
* Verifies that the generated table names are correct.
*/
protected function checkTableNames() {
$this->assertEquals('search_api_db_database_search_index_1', $this->getIndexDbInfo()['index_table']);
$this->assertEquals('search_api_db_database_search_index_text', $this->getIndexDbInfo()['field_tables']['body']['table']);
}
/**
* Verifies that the stored information about multi-valued fields is correct.
*/
protected function checkMultiValuedInfo() {
$db_info = $this->getIndexDbInfo();
$field_info = $db_info['field_tables'];
$fields = [
'name',
'body',
'type',
'keywords',
'category',
'width',
'search_api_datasource',
'search_api_language',
];
$multi_valued = [
'name',
'body',
'keywords',
];
foreach ($fields as $field_id) {
$this->assertArrayHasKey($field_id, $field_info, "Field info saved for field $field_id.");
if (in_array($field_id, $multi_valued)) {
$this->assertFalse(empty($field_info[$field_id]['multi-valued']), "Field $field_id is stored as multi-value.");
}
else {
$this->assertTrue(empty($field_info[$field_id]['multi-valued']), "Field $field_id is not stored as multi-value.");
}
}
}
/**
* Edits the server to enable partial matches.
*
* @param bool $enable
* (optional) Whether partial matching should be enabled or disabled.
*/
protected function editServerPartial($enable = TRUE) {
$server = $this->getServer();
$backend_config = $server->getBackendConfig();
$backend_config['partial_matches'] = $enable;
$server->setBackendConfig($backend_config);
$this->assertTrue((bool) $server->save(), 'The server was successfully edited.');
$this->resetEntityCache();
}
/**
* Tests whether partial searches work.
*/
protected function searchSuccessPartial() {
$results = $this->buildSearch('foobaz')->range(0, 1)->execute();
$this->assertResults([1], $results, 'Partial search for »foobaz«');
$results = $this->buildSearch('foo', [], [], FALSE)
->sort('search_api_relevance', QueryInterface::SORT_DESC)
->sort('id')
->execute();
$this->assertResults([1, 2, 4, 3, 5], $results, 'Partial search for »foo«');
$results = $this->buildSearch('foo tes')->execute();
$this->assertResults([1, 2, 3, 4], $results, 'Partial search for »foo tes«');
$results = $this->buildSearch('oob est')->execute();
$this->assertResults([1, 2, 3], $results, 'Partial search for »oob est«');
$results = $this->buildSearch('foo nonexistent')->execute();
$this->assertResults([], $results, 'Partial search for »foo nonexistent«');
$results = $this->buildSearch('bar nonexistent')->execute();
$this->assertResults([], $results, 'Partial search for »foo nonexistent«');
$keys = [
'#conjunction' => 'AND',
'oob',
[
'#conjunction' => 'OR',
'est',
'nonexistent',
],
];
$results = $this->buildSearch($keys)->execute();
$this->assertResults([1, 2, 3], $results, 'Partial search for complex keys');
$results = $this->buildSearch('foo', ['category,item_category'], [], FALSE)
->sort('id', QueryInterface::SORT_DESC)
->execute();
$this->assertResults([2, 1], $results, 'Partial search for »foo« with additional filter');
$query = $this->buildSearch();
$conditions = $query->createConditionGroup('OR');
$conditions->addCondition('name', 'test');
$conditions->addCondition('body', 'test');
$query->addConditionGroup($conditions);
$results = $query->execute();
$this->assertResults([1, 2, 3, 4], $results, 'Partial search with multi-field fulltext filter');
}
/**
* Edits the server to change the "Minimum word length" setting.
*/
protected function editServerMinChars() {
$server = $this->getServer();
$backend_config = $server->getBackendConfig();
$backend_config['min_chars'] = 4;
$backend_config['partial_matches'] = FALSE;
$server->setBackendConfig($backend_config);
$success = (bool) $server->save();
$this->assertTrue($success, 'The server was successfully edited.');
$this->clearIndex();
$this->indexItems($this->indexId);
$this->resetEntityCache();
}
/**
* Tests the results of some test searches with minimum word length of 4.
*/
protected function searchSuccessMinChars() {
$results = $this->getIndex()->query()->keys('test')->range(1, 2)->execute();
$this->assertEquals(4, $results->getResultCount(), 'Search for »test« returned correct number of results.');
$this->assertEquals($this->getItemIds([4, 1]), array_keys($results->getResultItems()), 'Search for »test« returned correct result.');
$this->assertEmpty($results->getIgnoredSearchKeys());
$this->assertEmpty($results->getWarnings());
$query = $this->buildSearch();
$conditions = $query->createConditionGroup('OR');
$conditions->addCondition('name', 'test');
$conditions->addCondition('body', 'test');
$query->addConditionGroup($conditions);
$results = $query->execute();
$this->assertResults([1, 2, 3, 4], $results, 'Search with multi-field fulltext filter');
$results = $this->buildSearch(NULL, ['body,test foobar'])->execute();
$this->assertResults([3], $results, 'Search with multi-term fulltext filter');
$results = $this->getIndex()->query()->keys('test foo')->execute();
$this->assertResults([2, 4, 1, 3], $results, 'Search for »test foo«', ['foo']);
$results = $this->buildSearch('foo', ['type,item'])->execute();
$this->assertResults([1, 2, 3], $results, 'Search for »foo«', ['foo'], ['No valid search keys were present in the query.']);
$keys = [
'#conjunction' => 'AND',
'test',
[
'#conjunction' => 'OR',
'baz',
'foobar',
],
[
'#conjunction' => 'OR',
'#negation' => TRUE,
'bar',
'fooblob',
],
];
$results = $this->buildSearch($keys)->execute();
$this->assertResults([3], $results, 'Complex search 1', ['baz', 'bar']);
$keys = [
'#conjunction' => 'AND',
'test',
[
'#conjunction' => 'OR',
'baz',
'foobar',
],
[
'#conjunction' => 'OR',
'#negation' => TRUE,
'bar',
'fooblob',
],
];
$results = $this->buildSearch($keys)->execute();
$this->assertResults([3], $results, 'Complex search 2', ['baz', 'bar']);
$results = $this->buildSearch(NULL, ['keywords,orange'])->execute();
$this->assertResults([1, 2, 5], $results, 'Filter query 1 on multi-valued field');
$conditions = [
'keywords,orange',
'keywords,apple',
];
$results = $this->buildSearch(NULL, $conditions)->execute();
$this->assertResults([2], $results, 'Filter query 2 on multi-valued field');
$results = $this->buildSearch()->addCondition('keywords', 'orange', '<>')->execute();
$this->assertResults([3, 4], $results, 'Negated filter on multi-valued field');
$results = $this->buildSearch()->addCondition('keywords', NULL)->execute();
$this->assertResults([3], $results, 'Query with NULL filter');
$results = $this->buildSearch()->addCondition('keywords', NULL, '<>')->execute();
$this->assertResults([1, 2, 4, 5], $results, 'Query with NOT NULL filter');
}
/**
* Checks that an unknown operator throws an exception.
*/
protected function checkUnknownOperator() {
try {
$this->buildSearch()
->addCondition('id', 1, '!=')
->execute();
$this->fail('Unknown operator "!=" did not throw an exception.');
}
catch (SearchApiException $e) {
$this->assertTrue(TRUE, 'Unknown operator "!=" threw an exception.');
}
}
/**
* Checks whether the module's specific alter hooks work correctly.
*/
protected function checkDbQueryAlter() {
$query = $this->buildSearch();
$query->setOption('search_api_test_db_search_api_db_query_alter', TRUE);
$results = $query->execute();
$this->assertResults([], $results, 'Query triggering custom alter hook');
}
/**
* Checks that field ID changes are treated correctly (without re-indexing).
*/
protected function checkFieldIdChanges() {
$this->getIndex()
->renameField('type', 'foobar')
->save();
$results = $this->buildSearch(NULL, ['foobar,item'])->execute();
$this->assertResults([1, 2, 3], $results, 'Search after renaming a field.');
$this->getIndex()->renameField('foobar', 'type')->save();
}
/**
* {@inheritdoc}
*/
protected function checkSecondServer() {
/** @var \Drupal\search_api\ServerInterface $second_server */
$second_server = Server::create([
'id' => 'test2',
'backend' => 'search_api_db',
'backend_config' => [
'database' => 'default:default',
],
]);
$second_server->save();
$query = $this->buildSearch();
try {
$second_server->search($query);
$this->fail('Could execute a query for an index on a different server.');
}
catch (SearchApiException $e) {
$this->assertTrue(TRUE, 'Executing a query for an index on a different server throws an exception.');
}
$second_server->delete();
}
/**
* Tests the case-sensitivity of fulltext searches.
*
* @see https://www.drupal.org/node/2557291
*/
protected function regressionTest2557291() {
$results = $this->buildSearch('case')->execute();
$this->assertResults([1], $results, 'Search for lowercase "case"');
$results = $this->buildSearch('Case')->execute();
$this->assertResults([1, 3], $results, 'Search for capitalized "Case"');
$results = $this->buildSearch('CASE')->execute();
$this->assertResults([], $results, 'Search for non-existent uppercase version of "CASE"');
$results = $this->buildSearch('föö')->execute();
$this->assertResults([1], $results, 'Search for keywords with umlauts');
$results = $this->buildSearch('smile' . json_decode('"\u1F601"'))->execute();
$this->assertResults([1], $results, 'Search for keywords with umlauts');
$results = $this->buildSearch()->addCondition('keywords', 'grape', '<>')->execute();
$this->assertResults([1, 3], $results, 'Negated filter on multi-valued field');
}
/**
* Tests searching for multiple two-letter words.
*
* @see https://www.drupal.org/node/2511860
*/
protected function regressionTest2511860() {
$query = $this->buildSearch();
$query->addCondition('body', 'ab xy');
$results = $query->execute();
$this->assertEquals(5, $results->getResultCount(), 'Fulltext filters on short words do not change the result.');
$query = $this->buildSearch();
$query->addCondition('body', 'ab ab');
$results = $query->execute();
$this->assertEquals(5, $results->getResultCount(), 'Fulltext filters on duplicate short words do not change the result.');
}
/**
* Tests changing a field boost to a floating point value.
*
* @see https://www.drupal.org/node/2846932
*/
protected function regressionTest2846932() {
$index = $this->getIndex();
$index->getField('body')->setBoost(0.8);
$index->save();
}
/**
* Tests indexing of text tokens with leading/trailing whitespace.
*
* @see https://www.drupal.org/node/2926733
*/
protected function regressionTest2926733() {
$index = $this->getIndex();
$item_id = $this->getItemIds([1])[0];
$fields_helper = \Drupal::getContainer()
->get('search_api.fields_helper');
$item = $fields_helper->createItem($index, $item_id);
$field = clone $index->getField('body');
$value = new TextValue('test');
$tokens = [];
foreach (['test', ' test', ' test', 'test ', ' test '] as $token) {
$tokens[] = new TextToken($token);
}
$value->setTokens($tokens);
$field->setValues([$value]);
$item->setFields([
'body' => $field,
]);
$item->setFieldsExtracted(TRUE);
$index->getServerInstance()->indexItems($index, [$item_id => $item]);
// Make sure to re-index the proper version of the item to avoid confusing
// the other tests.
list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id);
$index->trackItemsUpdated($datasource_id, [$raw_id]);
$this->indexItems($index->id());
}
/**
* Tests indexing of items with boost.
*
* @see https://www.drupal.org/node/2938646
*/
protected function regressionTest2938646() {
$db_info = $this->getIndexDbInfo();
$text_table = $db_info['field_tables']['body']['table'];
$item_id = $this->getItemIds([1])[0];
$select = \Drupal::database()->select($text_table, 't');
$select
->fields('t', ['score'])
->condition('item_id', $item_id)
->condition('word', 'test');
$select2 = clone $select;
// Check old score.
$old_score = $select
->execute()
->fetchField();
$this->assertNotSame(FALSE, $old_score);
$this->assertGreaterThan(0, $old_score);
// Re-index item with higher boost.
$index = $this->getIndex();
$item = $this->container->get('search_api.fields_helper')
->createItem($index, $item_id);
$item->setBoost(2);
$indexed_ids = $this->indexItemDirectly($index, $item);
$this->assertEquals([$item_id], $indexed_ids);
// Verify the field scores changed accordingly.
$new_score = $select2
->execute()
->fetchField();
$this->assertNotSame(FALSE, $new_score);
$this->assertEquals(2 * $old_score, $new_score);
}
/**
* {@inheritdoc}
*/
protected function checkIndexWithoutFields() {
$index = parent::checkIndexWithoutFields();
$expected = [
'search_api_datasource',
'search_api_language',
];
$db_info = $this->getIndexDbInfo($index->id());
$info_fields = array_keys($db_info['field_tables']);
sort($info_fields);
$this->assertEquals($expected, $info_fields);
return $index;
}
/**
* {@inheritdoc}
*/
protected function checkModuleUninstall() {
$db_info = $this->getIndexDbInfo();
$normalized_storage_table = $db_info['index_table'];
$field_tables = $db_info['field_tables'];
// See whether clearing the server works.
// Regression test for #2156151.
$server = $this->getServer();
$index = $this->getIndex();
$server->deleteAllIndexItems($index);
$query = $this->buildSearch();
$results = $query->execute();
$this->assertEquals(0, $results->getResultCount(), 'Clearing the server worked correctly.');
$schema = \Drupal::database()->schema();
$table_exists = $schema->tableExists($normalized_storage_table);
$this->assertTrue($table_exists, 'The index tables were left in place.');
// See whether disabling the index correctly removes all of its tables.
$index->disable()->save();
$db_info = $this->getIndexDbInfo();
$this->assertNull($db_info, 'The index was successfully removed from the server.');
$table_exists = $schema->tableExists($normalized_storage_table);
$this->assertFalse($table_exists, 'The index tables were deleted.');
foreach ($field_tables as $field_table) {
$table_exists = $schema->tableExists($field_table['table']);
$this->assertFalse($table_exists, "Field table {$field_table['table']} was successfully deleted.");
}
$index->enable()->save();
// Remove first the index and then the server.
$index->setServer();
$index->save();
$db_info = $this->getIndexDbInfo();
$this->assertNull($db_info, 'The index was successfully removed from the server.');
$table_exists = $schema->tableExists($normalized_storage_table);
$this->assertFalse($table_exists, 'The index tables were deleted.');
foreach ($field_tables as $field_table) {
$table_exists = $schema->tableExists($field_table['table']);
$this->assertFalse($table_exists, "Field table {$field_table['table']} was successfully deleted.");
}
// Re-add the index to see if the associated tables are also properly
// removed when the server is deleted.
$index->setServer($server);
$index->save();
$server->delete();
$db_info = $this->getIndexDbInfo();
$this->assertNull($db_info, 'The index was successfully removed from the server.');
$table_exists = $schema->tableExists($normalized_storage_table);
$this->assertFalse($table_exists, 'The index tables were deleted.');
foreach ($field_tables as $field_table) {
$table_exists = $schema->tableExists($field_table['table']);
$this->assertFalse($table_exists, "Field table {$field_table['table']} was successfully deleted.");
}
// Uninstall the module.
\Drupal::service('module_installer')->uninstall(['search_api_db'], FALSE);
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('search_api_db'), 'The Database Search module was successfully uninstalled.');
$tables = $schema->findTables('search_api_db_%');
$expected = [
'search_api_db_database_search_index' => 'search_api_db_database_search_index',
];
$this->assertEquals($expected, $tables, 'All the tables of the the Database Search module have been removed.');
}
/**
* Retrieves the database information for the test index.
*
* @param string|null $index_id
* (optional) The ID of the index whose database information should be
* retrieved.
*
* @return array
* The database information stored by the backend for the test index.
*/
protected function getIndexDbInfo($index_id = NULL) {
$index_id = $index_id ?: $this->indexId;
return \Drupal::keyValue(Database::INDEXES_KEY_VALUE_STORE_ID)
->get($index_id);
}
/**
* Indexes an item directly.
*
* @param \Drupal\search_api\IndexInterface $index
* The search index to index the item on.
* @param \Drupal\search_api\Item\ItemInterface $item
* The item.
*
* @return string[]
* The successfully indexed IDs.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if indexing failed.
*/
protected function indexItemDirectly(IndexInterface $index, ItemInterface $item) {
$items = [$item->getId() => $item];
// Minimalistic version of code copied from
// \Drupal\search_api\Entity\Index::indexSpecificItems().
$index->alterIndexedItems($items);
\Drupal::moduleHandler()->alter('search_api_index_items', $index, $items);
foreach ($items as $item) {
// This will cache the extracted fields so processors, etc., can retrieve
// them directly.
$item->getFields();
}
$index->preprocessIndexItems($items);
$indexed_ids = [];
if ($items) {
$indexed_ids = $index->getServerInstance()->indexItems($index, $items);
}
return $indexed_ids;
}
/**
* Tests whether a server on a non-default database is handled correctly.
*/
public function testNonDefaultDatabase() {
// Clone the primary credentials to a replica connection.
// Note this will result in two independent connection objects that happen
// to point to the same place.
// @see \Drupal\KernelTests\Core\Database\ConnectionTest::testConnectionRouting()
$connection_info = CoreDatabase::getConnectionInfo('default');
CoreDatabase::addConnectionInfo('default', 'replica', $connection_info['default']);
$db1 = CoreDatabase::getConnection('default', 'default');
$db2 = CoreDatabase::getConnection('replica', 'default');
// Safety checks copied from the Core test, if these fail something is wrong
// with Core.
$this->assertNotNull($db1, 'default connection is a real connection object.');
$this->assertNotNull($db2, 'replica connection is a real connection object.');
$this->assertNotSame($db1, $db2, 'Each target refers to a different connection.');
// Create backends based on each of the two targets and verify they use the
// right connections.
$config = [
'database' => 'default:default',
];
$backend1 = Database::create($this->container, $config, '', []);
$config['database'] = 'default:replica';
$backend2 = Database::create($this->container, $config, '', []);
$this->assertSame($db1, $backend1->getDatabase());
$this->assertSame($db2, $backend2->getDatabase());
// Make sure they also use different DBMS compatibility handlers, which also
// use the correct database connections.
$dbms_comp1 = $backend1->getDbmsCompatibilityHandler();
$dbms_comp2 = $backend2->getDbmsCompatibilityHandler();
$this->assertNotSame($dbms_comp1, $dbms_comp2);
$this->assertSame($db1, $dbms_comp1->getDatabase());
$this->assertSame($db2, $dbms_comp2->getDatabase());
// Finally, make sure the DBMS compatibility handlers also have the correct
// classes (meaning we used the correct one and didn't just fall back to the
// generic database).
$service = $this->container->get('search_api_db.database_compatibility');
$database_type = $db1->databaseType();
$service_id = "$database_type.search_api_db.database_compatibility";
$service2 = $this->container->get($service_id);
$this->assertSame($service2, $service);
$class = get_class($service);
$this->assertNotEquals(GenericDatabase::class, $class);
$this->assertSame($dbms_comp1, $service);
$this->assertEquals($class, get_class($dbms_comp2));
}
}
@@ -0,0 +1,18 @@
type: module
name: 'Search API Taxonomy Term Handlers'
description: 'Deprecated. You should uninstall this module.'
package: Search
# core: 8.x
hidden: true
configure: search_api.overview
dependencies:
- drupal:system (>=8.1)
- search_api:search_api
- drupal:taxonomy
- drupal:views
# Information added by Drupal.org packaging script on 2018-02-23
version: '8.x-1.7'
core: '8.x'
project: 'search_api'
datestamp: 1519387691
@@ -0,0 +1,24 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Search API Taxonomy Term Handlers module.
*/
use Drupal\Core\Url;
/**
* Implements hook_requirements().
*/
function search_api_views_taxonomy_requirements() {
$requirements['search_api_views_taxonomy'] = [
'title' => t('Search API Taxonomy Term Handlers'),
'value' => t('Out-dated'),
'description' => t('All functionality of the "Search API Taxonomy Term Handlers" module has been moved into the Search API module. You should <a href=":uninstall">uninstall this module</a>.', [
':uninstall' => Url::fromRoute('system.modules_uninstall')->toString(),
]),
'severity' => REQUIREMENT_WARNING,
];
return $requirements;
}
@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="search_api">
<description>Default PHP CodeSniffer configuration for the Search API module.</description>
<file>.</file>
<arg name="extensions" value="inc,install,module,php,profile,test,theme"/>
<!-- Only include specific sniffs that pass. This ensures that, if new sniffs are added, HEAD does not fail.-->
<!-- Drupal sniffs -->
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Drupal.Classes.ClassCreateInstance"/>
<rule ref="Drupal.Classes.ClassDeclaration"/>
<rule ref="Drupal.Classes.FullyQualifiedNamespace"/>
<rule ref="Drupal.Classes.InterfaceName"/>
<rule ref="Drupal.Classes.UnusedUseStatement"/>
<rule ref="Drupal.Classes.UseLeadingBackslash"/>
<rule ref="Drupal.CSS.ClassDefinitionNameSpacing"/>
<rule ref="Drupal.CSS.ColourDefinition"/>
<rule ref="Drupal.Commenting.ClassComment"/>
<rule ref="Drupal.Commenting.DataTypeNamespace" />
<rule ref="Drupal.Commenting.DocComment"/>
<rule ref="Drupal.Commenting.DocCommentStar"/>
<rule ref="Drupal.Commenting.FileComment"/>
<rule ref="Drupal.Commenting.FunctionComment"/>
<rule ref="Drupal.Commenting.InlineComment">
<!-- This is impractical when commenting code out. -->
<exclude name="Drupal.Commenting.InlineComment.InvalidEndChar" />
<!-- We (rarely) use comments as "headings" for multiple functions. -->
<exclude name="Drupal.Commenting.InlineComment.SpacingAfter" />
<!--
This disallows indentation in comments, even though it can sometimes be
helpful for structured explanations.
@see \Drupal\search_api\Plugin\search_api\processor\ContentAccess::addNodeAccess()
-->
<exclude name="Drupal.Commenting.InlineComment.SpacingBefore" />
</rule>
<rule ref="Drupal.Commenting.VariableComment">
<!-- This finds false positives when @code is used. -->
<exclude name="Drupal.Commenting.VariableComment.VarOrder"/>
</rule>
<rule ref="Drupal.Commenting.PostStatementComment"/>
<rule ref="Drupal.ControlStructures.ElseIf"/>
<rule ref="Drupal.ControlStructures.ControlSignature"/>
<rule ref="Drupal.ControlStructures.InlineControlStructure"/>
<rule ref="Drupal.Files.EndFileNewline"/>
<rule ref="Drupal.Files.FileEncoding"/>
<rule ref="Drupal.Files.TxtFileLineLength"/>
<rule ref="Drupal.Formatting.MultiLineAssignment"/>
<rule ref="Drupal.Formatting.SpaceInlineIf"/>
<rule ref="Drupal.Formatting.SpaceUnaryOperator"/>
<rule ref="Drupal.Functions.DiscouragedFunctions"/>
<rule ref="Drupal.Functions.FunctionDeclaration"/>
<rule ref="Drupal.InfoFiles.AutoAddedKeys"/>
<rule ref="Drupal.InfoFiles.ClassFiles"/>
<rule ref="Drupal.InfoFiles.DuplicateEntry"/>
<rule ref="Drupal.InfoFiles.Required"/>
<rule ref="Drupal.Methods.MethodDeclaration"/>
<rule ref="Drupal.NamingConventions.ValidVariableName">
<!-- This interferes with the stored entity properties. -->
<exclude name="Drupal.NamingConventions.ValidVariableName.LowerCamelName"/>
</rule>
<rule ref="Drupal.Scope.MethodScope"/>
<rule ref="Drupal.Semantics.EmptyInstall"/>
<rule ref="Drupal.Semantics.FunctionAlias"/>
<rule ref="Drupal.Semantics.FunctionT"/>
<rule ref="Drupal.Semantics.FunctionWatchdog"/>
<rule ref="Drupal.Semantics.InstallHooks"/>
<rule ref="Drupal.Semantics.LStringTranslatable"/>
<rule ref="Drupal.Semantics.PregSecurity"/>
<rule ref="Drupal.Semantics.TInHookMenu"/>
<rule ref="Drupal.Semantics.TInHookSchema"/>
<rule ref="Drupal.WhiteSpace.CloseBracketSpacing"/>
<rule ref="Drupal.WhiteSpace.Comma"/>
<rule ref="Drupal.WhiteSpace.EmptyLines"/>
<rule ref="Drupal.WhiteSpace.Namespace"/>
<rule ref="Drupal.WhiteSpace.ObjectOperatorIndent"/>
<rule ref="Drupal.WhiteSpace.ObjectOperatorSpacing"/>
<rule ref="Drupal.WhiteSpace.OpenBracketSpacing"/>
<rule ref="Drupal.WhiteSpace.OpenTagNewline"/>
<rule ref="Drupal.WhiteSpace.OperatorSpacing"/>
<rule ref="Drupal.WhiteSpace.ScopeClosingBrace"/>
<rule ref="Drupal.WhiteSpace.ScopeIndent"/>
<!-- Drupal Practice sniffs -->
<rule ref="DrupalPractice.Commenting.ExpectedException"/>
<!-- Generic sniffs -->
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings"/>
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie">
<properties>
<property name="checkClosures" value="true"/>
</properties>
</rule>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.LowerCaseKeyword"/>
<rule ref="Generic.PHP.UpperCaseConstant"/>
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<!-- MySource sniffs -->
<rule ref="MySource.Debug.DebugCode"/>
<!-- PEAR sniffs -->
<rule ref="PEAR.Files.IncludingFile"/>
<!-- Disable some error messages that we do not want. -->
<rule ref="PEAR.Files.IncludingFile.UseIncludeOnce">
<severity>0</severity>
</rule>
<rule ref="PEAR.Files.IncludingFile.UseInclude">
<severity>0</severity>
</rule>
<rule ref="PEAR.Files.IncludingFile.UseRequireOnce">
<severity>0</severity>
</rule>
<rule ref="PEAR.Files.IncludingFile.UseRequire">
<severity>0</severity>
</rule>
<rule ref="PEAR.Functions.ValidDefaultValue"/>
<!-- PEAR sniffs -->
<rule ref="PEAR.Functions.FunctionCallSignature"/>
<!-- The sniffs inside PEAR.Functions.FunctionCallSignature silenced below are
also silenced in Drupal CS' ruleset.xml. The code below is a 1-on-1 copy
from that file. -->
<!-- Disable some error messages that we already cover. -->
<rule ref="PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket">
<severity>0</severity>
</rule>
<rule ref="PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket">
<severity>0</severity>
</rule>
<!-- Disable some error messages that we do not want. -->
<rule ref="PEAR.Functions.FunctionCallSignature.Indent">
<severity>0</severity>
</rule>
<rule ref="PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket">
<severity>0</severity>
</rule>
<rule ref="PEAR.Functions.FunctionCallSignature.CloseBracketLine">
<severity>0</severity>
</rule>
<rule ref="PEAR.Functions.FunctionCallSignature.EmptyLine">
<severity>0</severity>
</rule>
<!-- PSR-2 sniffs -->
<rule ref="PSR2.Classes.PropertyDeclaration"/>
<rule ref="PSR2.Namespaces.NamespaceDeclaration"/>
<rule ref="PSR2.Namespaces.UseDeclaration"/>
<!-- Squiz sniffs -->
<rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
<rule ref="Squiz.Arrays.ArrayDeclaration">
<exclude name="Squiz.Arrays.ArrayDeclaration.NoKeySpecified"/>
<exclude name="Squiz.Arrays.ArrayDeclaration.KeySpecified"/>
</rule>
<!-- Disable some error messages that we do not want. -->
<rule ref="Squiz.Arrays.ArrayDeclaration.CloseBraceNotAligned">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.FirstValueNoNewline">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.KeyNotAligned">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.MultiLineNotAllowed">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.NoComma">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.NotLowerCase">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.ValueNotAligned">
<severity>0</severity>
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline">
<severity>0</severity>
</rule>
<rule ref="Squiz.ControlStructures.ForEachLoopDeclaration"/>
<!-- Disable some error messages that we already cover. -->
<rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.AsNotLower">
<severity>0</severity>
</rule>
<rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceAfterOpen">
<severity>0</severity>
</rule>
<rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceBeforeClose">
<severity>0</severity>
</rule>
<rule ref="Squiz.ControlStructures.ForLoopDeclaration"/>
<!-- Disable some error messages that we already cover. -->
<rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingAfterOpen">
<severity>0</severity>
</rule>
<rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingBeforeClose">
<severity>0</severity>
</rule>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine">
<severity>0</severity>
</rule>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace">
<severity>0</severity>
</rule>
<!-- Standard yet to be finalized on this (https://www.drupal.org/node/1539712). -->
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing">
<severity>0</severity>
</rule>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.Indent">
<severity>0</severity>
</rule>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine">
<severity>0</severity>
</rule>
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
<properties>
<property name="equalsSpacing" value="1"/>
</properties>
</rule>
<rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.NoSpaceBeforeArg">
<severity>0</severity>
</rule>
<rule ref="Squiz.PHP.LowercasePHPFunctions"/>
<rule ref="Squiz.Strings.ConcatenationSpacing">
<properties>
<property name="spacing" value="1"/>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.LanguageConstructSpacing" />
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
<!-- Zend sniffs -->
<rule ref="Zend.Files.ClosingTag"/>
</ruleset>
@@ -0,0 +1,354 @@
<?php
/**
* @file
* Hooks provided by the Search API module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the available Search API backends.
*
* Modules may implement this hook to alter the information that defines Search
* API backends. All properties that are available in
* \Drupal\search_api\Annotation\SearchApiBackend can be altered here, with the
* addition of the "class" and "provider" keys.
*
* @param array $backend_info
* The Search API backend info array, keyed by backend ID.
*
* @see \Drupal\search_api\Backend\BackendPluginBase
*/
function hook_search_api_backend_info_alter(array &$backend_info) {
foreach ($backend_info as $id => $info) {
$backend_info[$id]['class'] = '\Drupal\my_module\MyBackendDecorator';
$backend_info[$id]['example_original_class'] = $info['class'];
}
}
/**
* Alter the features a given search server supports.
*
* @param string[] $features
* The features supported by the server's backend.
* @param \Drupal\search_api\ServerInterface $server
* The search server in question.
*
* @see \Drupal\search_api\Backend\BackendSpecificInterface::getSupportedFeatures()
*/
function hook_search_api_server_features_alter(array &$features, \Drupal\search_api\ServerInterface $server) {
if ($server->getBackend() instanceof \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend) {
$features[] = 'my_custom_feature';
}
}
/**
* Alter the available datasources.
*
* Modules may implement this hook to alter the information that defines
* datasources. All properties that are available in
* \Drupal\search_api\Annotation\SearchApiDatasource can be altered here, with
* the addition of the "class" and "provider" keys.
*
* @param array $infos
* The datasource info array, keyed by datasource IDs.
*
* @see \Drupal\search_api\Datasource\DatasourcePluginBase
*/
function hook_search_api_datasource_info_alter(array &$infos) {
// I'm a traditionalist, I want them called "nodes"!
$infos['entity:node']['label'] = t('Node');
}
/**
* Alter the available processors.
*
* Modules may implement this hook to alter the information that defines
* processors. All properties that are available in
* \Drupal\search_api\Annotation\SearchApiProcessor can be altered here, with
* the addition of the "class" and "provider" keys.
*
* @param array $processors
* The processor information to be altered, keyed by processor IDs.
*
* @see \Drupal\search_api\Processor\ProcessorPluginBase
*/
function hook_search_api_processor_info_alter(array &$processors) {
if (!empty($processors['example_processor'])) {
$processors['example_processor']['class'] = '\Drupal\my_module\MuchBetterExampleProcessor';
}
}
/**
* Alter the available data types.
*
* @param array $data_type_definitions
* The definitions of the data type plugins.
*
* @see \Drupal\search_api\DataType\DataTypePluginBase
*/
function hook_search_api_data_type_info_alter(array &$data_type_definitions) {
if (isset($data_type_definitions['text'])) {
$data_type_definitions['text']['label'] = t('Parsed text');
}
}
/**
* Alter the available parse modes.
*
* @param array $parse_mode_definitions
* The definitions of the data type plugins.
*
* @see \Drupal\search_api\ParseMode\ParseModePluginBase
*/
function hook_search_api_parse_mode_info_alter(array &$parse_mode_definitions) {
if (isset($parse_mode_definitions['direct'])) {
$parse_mode_definitions['direct']['label'] = t('Solr syntax');
}
}
/**
* Alter the tracker info.
*
* @param array $tracker_info
* The Search API tracker info array, keyed by tracker ID.
*
* @see \Drupal\search_api\Tracker\TrackerPluginBase
*/
function hook_search_api_tracker_info_alter(array &$tracker_info) {
if (isset($tracker_info['default'])) {
$tracker_info['default']['example_original_class'] = $tracker_info['default']['class'];
$tracker_info['default']['class'] = '\Drupal\my_module\Plugin\search_api\tracker\MyCustomImplementationTracker';
}
}
/**
* Alter the list of known search displays.
*
* @param array $displays
* The Search API display info array, keyed by display ID.
*
* @see \Drupal\search_api\Display\DisplayPluginBase
*/
function hook_search_api_displays_alter(array &$displays) {
if (isset($displays['some_key'])) {
$displays['some_key']['label'] = t('New label for existing Display');
}
}
/**
* Alter the mapping of Drupal data types to Search API data types.
*
* @param array $mapping
* An array mapping all known (and supported) Drupal data types to their
* corresponding Search API data types. A value of FALSE means that fields of
* that type should be ignored by the Search API.
*
* @see \Drupal\search_api\Utility\DataTypeHelperInterface::getFieldTypeMapping()
*/
function hook_search_api_field_type_mapping_alter(array &$mapping) {
$mapping['duration_iso8601'] = FALSE;
$mapping['my_new_type'] = 'string';
}
/**
* Alter the mapping of Search API data types to their default Views handlers.
*
* Field handlers are not determined by these simplified (Search API) types, but
* by their actual property data types. For altering that mapping, see
* hook_search_api_views_field_handler_mapping_alter().
*
* @param array $mapping
* An associative array with data types as the keys and Views table data
* definition items as the values. In addition to all normally defined Search
* API data types, keys can also be "options" for any field with an options
* list, "entity" for general entity-typed fields or "entity:ENTITY_TYPE"
* (with "ENTITY_TYPE" being the machine name of an entity type) for entities
* of that type.
*/
function hook_search_api_views_handler_mapping_alter(array &$mapping) {
$mapping['entity:my_entity_type'] = [
'argument' => [
'id' => 'my_entity_type',
],
'filter' => [
'id' => 'my_entity_type',
],
'sort' => [
'id' => 'my_entity_type',
],
];
$mapping['date']['filter']['id'] = 'my_date_filter';
}
/**
* Alter the mapping of property types to their default Views field handlers.
*
* This is used in the Search API Views integration to create Search
* API-specific field handlers for all properties of datasources and some entity
* types.
*
* In addition to the definition returned here, for Field API fields, the
* "field_name" will be set to the field's machine name.
*
* @param array $mapping
* An associative array with property data types as the keys and Views field
* handler definitions as the values (that is, just the inner "field" portion
* of Views data definition items). In some cases the value might also be NULL
* instead, to indicate that properties of this type shouldn't have field
* handlers. The data types in the keys might also contain asterisks (*) as
* wildcard characters. Data types with wildcards will be matched only if no
* specific type exists, and longer type patterns will be tried before shorter
* ones. The "*" mapping therefore is the default if no other match could be
* found.
*/
function hook_search_api_views_field_handler_mapping_alter(array &$mapping) {
$mapping['field_item:string_long'] = [
'id' => 'example_field',
];
$mapping['example_property_type'] = [
'id' => 'example_field',
'some_option' => 'foo',
];
}
/**
* Allows you to log or alter the items that are indexed.
*
* Please be aware that generally preventing the indexing of certain items is
* deprecated. This is better done with processors, which can easily be
* configured and only added to indexes where this behaviour is wanted.
* If your module will use this hook to reject certain items from indexing,
* please document this clearly to avoid confusion.
*
* @param \Drupal\search_api\IndexInterface $index
* The search index on which items will be indexed.
* @param \Drupal\search_api\Item\ItemInterface[] $items
* The items that will be indexed.
*/
function hook_search_api_index_items_alter(\Drupal\search_api\IndexInterface $index, array &$items) {
foreach ($items as $item_id => $item) {
list(, $raw_id) = \Drupal\search_api\Utility\Utility::splitCombinedId($item->getId());
if ($raw_id % 5 == 0) {
unset($items[$item_id]);
}
}
$arguments = [
'%index' => $index->label(),
'@ids' => implode(', ', array_keys($items)),
];
drupal_set_message(t('Indexing items on index %index with the following IDs: @ids', $arguments));
}
/**
* React after items were indexed.
*
* @param \Drupal\search_api\IndexInterface $index
* The used index.
* @param array $item_ids
* An array containing the successfully indexed items' IDs.
*/
function hook_search_api_items_indexed(\Drupal\search_api\IndexInterface $index, array $item_ids) {
if ($index->isValidDatasource('entity:node')) {
// Note that this is just an example, and would only work if there are only
// nodes indexed in that index (and even then the printed IDs would probably
// not be as expected).
drupal_set_message(t('Nodes indexed: @ids.', implode(', ', $item_ids)));
}
}
/**
* Alter a search query before it gets executed.
*
* The hook is invoked after all enabled processors have preprocessed the query.
*
* @param \Drupal\search_api\Query\QueryInterface $query
* The query that will be executed.
*/
function hook_search_api_query_alter(\Drupal\search_api\Query\QueryInterface &$query) {
// Do not run for queries with a certain tag.
if ($query->hasTag('example_tag')) {
return;
}
// Otherwise, exclude the node with ID 10 from the search results.
$fields = $query->getIndex()->getFields();
foreach ($query->getIndex()->getDatasources() as $datasource_id => $datasource) {
if ($datasource->getEntityTypeId() == 'node') {
if (isset($fields['nid'])) {
$query->addCondition('nid', 10, '<>');
}
}
}
}
/**
* Alter a search query with a specific tag before it gets executed.
*
* The hook is invoked after all enabled processors have preprocessed the query.
*
* @param \Drupal\search_api\Query\QueryInterface $query
* The query that will be executed.
*/
function hook_search_api_query_TAG_alter(\Drupal\search_api\Query\QueryInterface &$query) {
// Exclude the node with ID 10 from the search results.
$fields = $query->getIndex()->getFields();
foreach ($query->getIndex()->getDatasources() as $datasource_id => $datasource) {
if ($datasource->getEntityTypeId() == 'node') {
if (isset($fields['nid'])) {
$query->addCondition('nid', 10, '<>');
}
}
}
}
/**
* Alter a search query's result set.
*
* The hook is invoked after all enabled processors have postprocessed the
* results.
*
* @param \Drupal\search_api\Query\ResultSetInterface $results
* The search results to alter.
*/
function hook_search_api_results_alter(\Drupal\search_api\Query\ResultSetInterface &$results) {
$results->setExtraData('example_hook_invoked', microtime(TRUE));
}
/**
* Alter the result set of a search query with a specific tag.
*
* The hook is invoked after all enabled processors have postprocessed the
* results.
*
* @param \Drupal\search_api\Query\ResultSetInterface $results
* The search results to alter.
*/
function hook_search_api_results_TAG_alter(\Drupal\search_api\Query\ResultSetInterface &$results) {
$results->setExtraData('example_hook_invoked', microtime(TRUE));
}
/**
* React when a search index was scheduled for reindexing.
*
* @param \Drupal\search_api\IndexInterface $index
* The index scheduled for reindexing.
* @param bool $clear
* Boolean indicating whether the index was also cleared.
*/
function hook_search_api_index_reindex(\Drupal\search_api\IndexInterface $index, $clear = FALSE) {
\Drupal\Core\Database\Database::getConnection()->insert('example_search_index_reindexed')
->fields([
'index' => $index->id(),
'clear' => $clear,
'update_time' => \Drupal::time()->getRequestTime(),
])
->execute();
}
/**
* @} End of "addtogroup hooks".
*/
@@ -0,0 +1,449 @@
<?php
/**
* @file
* Drush commands for Search API.
*/
use Drupal\search_api\ConsoleException;
use Drupal\search_api\Utility\CommandHelper;
/**
* Implements hook_drush_command().
*/
function search_api_drush_command() {
$items = [];
$index['index_id'] = dt('The machine name of an index');
$server['server_id'] = dt('The machine name of a server');
$items['search-api-list'] = [
'description' => 'List all search indexes.',
'examples' => [
'drush search-api-list' => dt('List all search indexes.'),
'drush sapi-l' => dt('Alias to list all search indexes.'),
],
'aliases' => ['sapi-l'],
];
$items['search-api-enable'] = [
'description' => 'Enable one or more disabled search indexes.',
'examples' => [
'drush search-api-enable node_index' => dt('Enable the search index with the ID @name.', ['@name' => 'node_index']),
'drush sapi-en node_index' => dt('Alias to enable the search index with the ID @name.', ['@name' => 'node_index']),
],
'arguments' => $index,
'aliases' => ['sapi-en'],
];
$items['search-api-enable-all'] = [
'description' => 'Enable all disabled search indexes.',
'examples' => [
'drush search-api-enable-all' => dt('Enable all disabled indexes.'),
'drush sapi-ena' => dt('Alias to enable all disabled indexes.'),
],
'arguments' => [],
'aliases' => ['sapi-ena'],
];
$items['search-api-disable'] = [
'description' => 'Disable one or more enabled search indexes.',
'examples' => [
'drush search-api-disable node_index' => dt('Disable the search index with the ID @name.', ['@name' => 'node_index']),
'drush sapi-dis node_index' => dt('Alias to disable the search index with the ID @name.', ['@name' => 'node_index']),
],
'arguments' => $index,
'aliases' => ['sapi-dis'],
];
$items['search-api-disable-all'] = [
'description' => 'Disable all enabled search indexes.',
'examples' => [
'drush search-api-disable-all' => dt('Disable all enabled indexes.'),
'drush sapi-disa' => dt('Alias to disable all enabled indexes.'),
],
'arguments' => [],
'aliases' => ['sapi-disa'],
];
$items['search-api-status'] = [
'description' => 'Show the status of one or all search indexes.',
'examples' => [
'drush search-api-status' => dt('Show the status of all search indexes.'),
'drush sapi-s' => dt('Alias to show the status of all search indexes.'),
'drush sapi-s node_index' => dt('Show the status of the search index with the ID @name.', ['@name' => 'node_index']),
],
'arguments' => $index,
'aliases' => ['sapi-s'],
];
$items['search-api-index'] = [
'description' => 'Index items for one or all enabled search indexes.',
'examples' => [
'drush search-api-index' => dt('Index all items for all enabled indexes.'),
'drush sapi-i' => dt('Alias to index all items for all enabled indexes.'),
'drush sapi-i node_index' => dt('Index all items for the index with the ID @name.', ['@name' => 'node_index']),
'drush sapi-i node_index 100' => dt('Index a maximum number of @limit items for the index with the ID @name.', ['@limit' => 100, '@name' => 'node_index']),
'drush sapi-i node_index 100 10' => dt('Index a maximum number of @limit items (@batch_size items per batch run) for the index with the ID @name.', ['@limit' => 100, '@batch_size' => 10, '@name' => 'node_index']),
],
'options' => [
'limit' => dt('The number of items to index. Set to 0 to index all items. Defaults to 0 (index all).'),
'batch-size' => dt('The number of items to index per batch run. Set to 0 to index all items at once. Defaults to the "!batch_size_label" setting of the index.', ['!batch_size_label' => dt('Cron batch size')]),
],
'arguments' => $index,
'aliases' => ['sapi-i'],
];
$items['search-api-reset-tracker'] = [
'description' => 'Force reindexing of one or all search indexes, without deleting existing index data.',
'examples' => [
'drush search-api-reindex' => dt('Schedule all search indexes for reindexing.'),
'drush sapi-r' => dt('Alias to schedule all search indexes for reindexing .'),
'drush sapi-r node_index' => dt('Schedule the search index with the ID @name for reindexing.', ['@name' => 'node_index']),
],
'options' => [
'entity-types' => [
'description' => dt('List of entity type ids to reset tracker for.'),
'example_value' => 'user,node',
],
],
'arguments' => $index,
'aliases' => [
'search-api-mark-all',
'search-api-reindex',
'sapi-r',
],
];
$items['search-api-clear'] = [
'description' => 'Clear one or all search indexes and mark them for reindexing.',
'examples' => [
'drush search-api-clear' => dt('Clear all search indexes.'),
'drush sapi-c' => dt('Alias to clear all search indexes.'),
'drush sapi-c node_index' => dt('Clear the search index with the ID @name.', ['@name' => 'node_index']),
],
'arguments' => $index,
'aliases' => ['sapi-c'],
];
$items['search-api-search'] = [
'description' => 'Search for a keyword or phrase in a given index.',
'examples' => [
'drush search-api-search node_index title' => dt('Search for "title" inside the "node_index" index.'),
'drush sapi-search node_index title' => dt('Alias to search for "title" inside the "node_index" index.'),
],
'arguments' => $index + [
'keyword' => dt('The keyword to look for.'),
],
'aliases' => ['sapi-search'],
];
$items['search-api-server-list'] = [
'description' => 'List all search servers.',
'examples' => [
'drush search-api-server-list' => dt('List all search servers.'),
'drush sapi-sl' => dt('Alias to list all search servers.'),
],
'aliases' => ['sapi-sl'],
];
$items['search-api-server-enable'] = [
'description' => 'Enable a search server.',
'examples' => [
'drush search-api-server-e my_solr_server' => dt('Enable the @server search server.', ['@server' => 'my_solr_server']),
'drush sapi-se my_solr_server' => dt('Alias to enable the @server search server.', ['@server' => 'my_solr_server']),
],
'arguments' => $server,
'aliases' => ['sapi-se'],
];
$items['search-api-server-disable'] = [
'description' => 'Disable a search server.',
'examples' => [
'drush search-api-server-disable' => dt('Disable the @server search server.', ['@server' => 'my_solr_server']),
'drush sapi-sd' => dt('Alias to disable the @server search server.', ['@server' => 'my_solr_server']),
],
'arguments' => $server,
'aliases' => ['sapi-sd'],
];
$items['search-api-server-clear'] = [
'description' => 'Clear all search indexes on the search server and mark them for reindexing.',
'examples' => [
'drush search-api-server-clear' => dt('Clear all search indexes on the search server @server.', ['@server' => 'my_solr_server']),
'drush sapi-sc' => dt('Alias to clear all search indexes on the search server @server.', ['@server' => 'my_solr_server']),
],
'arguments' => $server,
'aliases' => ['sapi-sc'],
];
$items['search-api-set-index-server'] = [
'description' => 'Set the search server used by a given index.',
'examples' => [
'drush search-api-set-index-server default_node_index my_solr_server' => dt('Set the @index index to used the @server server.', ['@index' => 'default_node_index', '@server' => 'my_solr_server']),
'drush sapi-sis default_node_index my_solr_server' => dt('Alias to set the @index index to used the @server server.', ['@index' => 'default_node_index', '@server' => 'my_solr_server']),
],
'arguments' => $index + $server,
'aliases' => ['sapi-sis'],
];
return $items;
}
/**
* Prints a list of all search indexes.
*/
function drush_search_api_list() {
$command_helper = _search_api_drush_command_helper();
$rows[] = [
dt('ID'),
dt('Name'),
dt('Server'),
dt('Type'),
dt('Status'),
dt('Limit'),
];
$rows += $command_helper->indexListCommand();
foreach ($rows as &$row) {
$row['types'] = is_array($row['types']) ? implode(', ', $row['types']) : $row['types'];
$row['typeNames'] = is_array($row['types']) ? implode(', ', $row['typeNames']) : $row['types'];
}
drush_print_table($rows);
}
/**
* Enables one or more search indexes.
*
* @param string|null $index_id
* The ID of a search index to enable. Or NULL (only used internally) to
* enable all disabled indexes.
*/
function drush_search_api_enable($index_id = NULL) {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->enableIndexCommand([$index_id]);
}
catch (ConsoleException $exception) {
drush_set_error($exception->getMessage());
}
}
/**
* Enables all search indexes.
*/
function drush_search_api_enable_all() {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->enableIndexCommand();
}
catch (ConsoleException $exception) {
drush_set_error($exception->getMessage());
}
}
/**
* Disables one or more search indexes.
*
* @param string|null $index_id
* The ID of a search index to disable. Or NULL (only used internally) to
* disable all enabled indexes.
*/
function drush_search_api_disable($index_id = NULL) {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->disableIndexCommand([$index_id]);
}
catch (ConsoleException $exception) {
drush_set_error($exception->getMessage());
}
}
/**
* Disables all search indexes.
*/
function drush_search_api_disable_all() {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->disableIndexCommand();
}
catch (ConsoleException $exception) {
drush_set_error($exception->getMessage());
}
}
/**
* Displays the status of one or all search indexes.
*
* @param string|null $index_id
* (optional) The ID of the search index whose status should be displayed, or
* NULL to display the status of all search indexes.
*/
function drush_search_api_status($index_id = NULL) {
$command_helper = _search_api_drush_command_helper();
$rows[] = [
dt('ID'),
dt('Name'),
dt('% Complete'),
dt('Indexed'),
dt('Total'),
];
$rows += $command_helper->indexStatusCommand([$index_id]);
drush_print_table($rows);
}
/**
* Indexes items.
*
* @param string|null $index_id
* (optional) The index ID for which items should be indexed, or NULL to index
* items on all indexes.
*/
function drush_search_api_index($index_id = NULL) {
$command_helper = _search_api_drush_command_helper();
$limit = drush_get_option('limit');
$batch_size = drush_get_option('batch-size');
$batch_set = $command_helper->indexItemsToIndexCommand([$index_id], $limit, $batch_size);
if ($batch_set) {
drush_backend_batch_process();
}
}
/**
* Schedules a search index for reindexing.
*
* @param string|null $index_id
* (optional) The index ID for which items should be reindexed, or NULL to
* reindex all search indexes.
*/
function drush_search_api_reset_tracker($index_id = NULL) {
$command_helper = _search_api_drush_command_helper();
$entity_types = drush_get_option_list('entity-types');
$command_helper->resetTrackerCommand([$index_id], $entity_types);
}
/**
* Clears a search index.
*
* @param string|null $index_id
* (optional) The ID of the search index which should be cleared, or NULL to
* clear all search indexes.
*/
function drush_search_api_clear($index_id = NULL) {
$command_helper = _search_api_drush_command_helper();
$command_helper->clearIndexCommand([$index_id]);
}
/**
* Executes a simple keyword search and displays the results in a table.
*
* @param string $index_id
* The ID of the index being searched.
* @param string $keyword
* The search keyword.
*/
function drush_search_api_search($index_id, $keyword) {
$command_helper = _search_api_drush_command_helper();
$rows = $command_helper->searchIndexCommand($index_id, $keyword);
drush_print_table($rows);
}
/**
* Lists all available search servers.
*/
function drush_search_api_server_list() {
$command_helper = _search_api_drush_command_helper();
$rows[] = [
dt('ID'),
dt('Name'),
dt('Status'),
];
try {
$rows += $command_helper->serverListCommand();
}
catch (ConsoleException $exception) {
drush_print($exception->getMessage());
}
drush_print_table($rows);
}
/**
* Enables a search server.
*
* @param string $server_id
* The ID of the server to enable.
*/
function drush_search_api_server_enable($server_id = NULL) {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->enableServerCommand($server_id);
}
catch (ConsoleException $exception) {
drush_print($exception->getMessage());
}
}
/**
* Disables a search server.
*
* @param string $server_id
* The ID of the server to disable.
*/
function drush_search_api_server_disable($server_id = NULL) {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->disableServerCommand($server_id);
}
catch (ConsoleException $exception) {
drush_print($exception->getMessage());
}
}
/**
* Clears all search indexes on the server and marks them for reindexing.
*
* @param string $server_id
* The ID of the server to clear all search indexes.
*/
function drush_search_api_server_clear($server_id = NULL) {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->clearServerCommand($server_id);
}
catch (ConsoleException $exception) {
drush_print($exception->getMessage());
}
}
/**
* Sets the server for a given index.
*
* @param string $index_id
* The ID of the index whose server should be changed.
* @param string $server_id
* The ID of the new server for the index.
*/
function drush_search_api_set_index_server($index_id = NULL, $server_id = NULL) {
$command_helper = _search_api_drush_command_helper();
try {
$command_helper->setIndexServerCommand($index_id, $server_id);
}
catch (ConsoleException $exception) {
drush_print($exception->getMessage());
}
}
/**
* Returns an instance of the command helper.
*
* @return \Drupal\search_api\Utility\CommandHelper
* An instance of the command helper class.
*/
function _search_api_drush_command_helper() {
$command_helper = new CommandHelper(\Drupal::entityTypeManager(), \Drupal::moduleHandler(), 'dt');
$command_helper->setLogger(\Drupal::logger('search_api'));
return $command_helper;
}
@@ -0,0 +1,14 @@
type: module
name: 'Search API'
description: 'Provides a generic framework for modules offering search capabilities.'
package: Search
# core: 8.x
configure: search_api.overview
dependencies:
- system (>=8.4)
# Information added by Drupal.org packaging script on 2018-02-23
version: '8.x-1.7'
core: '8.x'
project: 'search_api'
datestamp: 1519387691
@@ -0,0 +1,281 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Search API module.
*/
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Link;
use Drupal\search_api\Entity\Server;
use Drupal\Core\Url;
/**
* Implements hook_schema().
*/
function search_api_schema() {
$schema['search_api_item'] = [
'description' => 'Stores the items which should be indexed for each index, and their state.',
'fields' => [
'index_id' => [
'description' => 'The ID of the index this item belongs to',
'type' => 'varchar',
'length' => 50,
'not null' => TRUE,
],
'datasource' => [
'description' => 'The plugin ID of the datasource this item belongs to',
'type' => 'varchar',
'length' => 50,
'not null' => TRUE,
],
'item_id' => [
'description' => 'The unique identifier of this item',
'type' => 'varchar',
'length' => 150,
'not null' => TRUE,
],
'changed' => [
'description' => 'A timestamp indicating when the item was last changed',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'status' => [
'description' => 'Boolean indicating the reindexation status, "1" when we need to reindex, "0" otherwise',
'type' => 'int',
'not null' => TRUE,
],
],
'indexes' => [
'indexing' => ['index_id', 'status', 'changed', 'item_id'],
],
'primary key' => ['index_id', 'item_id'],
];
return $schema;
}
/**
* Implements hook_uninstall().
*/
function search_api_uninstall() {
\Drupal::state()->delete('search_api_use_tracking_batch');
foreach (\Drupal::configFactory()->listAll('search_api.index.') as $index_id) {
\Drupal::state()->delete("search_api.index.$index_id.has_reindexed");
}
}
/**
* Implements hook_requirements().
*/
function search_api_requirements($phase) {
if ($phase == 'runtime') {
$requirements = [];
$message = _search_api_search_module_warning();
if ($message) {
$requirements += [
'search_api_core_search' => [
'title' => t('Search API'),
'value' => $message,
'severity' => REQUIREMENT_WARNING,
],
];
}
/** @var \Drupal\search_api\ServerInterface[] $servers */
$servers = Server::loadMultiple();
$unavailable_servers = [];
foreach ($servers as $server) {
if ($server->status() && !$server->isAvailable()) {
$unavailable_servers[] = $server->label();
}
}
if (!empty($unavailable_servers)) {
$requirements += [
'search_api_server_unavailable' => [
'title' => t('Search API'),
'value' => \Drupal::translation()->formatPlural(
count($unavailable_servers),
'The search server "@servers" is currently not available',
'The following search servers are not available: @servers',
['@servers' => implode(', ', $unavailable_servers)]
),
'severity' => REQUIREMENT_ERROR
]
];
}
$pending_tasks = \Drupal::getContainer()
->get('search_api.task_manager')
->getTasksCount();
if ($pending_tasks) {
$args['@link'] = '';
$url = Url::fromRoute('search_api.execute_tasks');
if ($url->access()) {
$link = new Link(t('Execute now'), $url);
$link = $link->toString();
$args['@link'] = $link;
$args['@link'] = new FormattableMarkup(' (@link)', $args);
}
$requirements['search_api_pending_tasks'] = [
'title' => t('Search API'),
'value' => \Drupal::translation()->formatPlural(
$pending_tasks,
'There is @count pending Search API task. @link',
'There are @count pending Search API tasks. @link',
$args
),
'severity' => REQUIREMENT_WARNING,
];
}
return $requirements;
}
return [];
}
/**
* Adapts index config schema to remove an unnecessary layer for plugins.
*/
function search_api_update_8101() {
// This update function updates search indexes for the change from
// https://www.drupal.org/node/2656052.
$config_factory = \Drupal::configFactory();
$plugin_types = [
'processor',
'datasource',
'tracker',
];
foreach ($config_factory->listAll('search_api.index.') as $index_id) {
$index = $config_factory->getEditable($index_id);
$changed = FALSE;
foreach ($plugin_types as $plugin_type) {
$property = $plugin_type . '_settings';
$plugins = $index->get($property);
foreach ($plugins as $id => $config) {
if (isset($config['plugin_id']) && isset($config['settings'])) {
$changed = TRUE;
$plugins[$id] = $config['settings'];
}
}
$index->set($property, $plugins);
}
if ($changed) {
// Mark the resulting configuration as trusted data. This avoids issues
// with future schema changes.
$index->save(TRUE);
}
}
return t('Index config schema updated.');
}
/**
* Removes unsupported cache plugins from Search API views.
*/
function search_api_update_8102() {
$config_factory = \Drupal::configFactory();
$changed = [];
foreach ($config_factory->listAll('views.view.') as $view_config_name) {
$view = $config_factory->getEditable($view_config_name);
$displays = $view->get('display');
if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
$change = FALSE;
foreach ($displays as $id => $display) {
if (!empty($display['display_options']['cache']['type']) && in_array($display['display_options']['cache']['type'], ['tag', 'time'])
) {
$displays[$id]['display_options']['cache']['type'] = 'none';
$change = TRUE;
}
}
if ($change) {
$view->set('display', $displays);
// Mark the resulting configuration as trusted data. This avoids issues
// with future schema changes.
$view->save(TRUE);
$changed[] = $view->get('id');
}
}
}
if (!empty($changed)) {
return \Drupal::translation()->translate('Removed incompatible cache options for the following Search API-based views: @ids', ['@ids' => implode(', ', array_unique($changed))]);
}
return NULL;
}
/**
* Switches from the old "Node status" to the new "Entity status" processor.
*/
function search_api_update_8103() {
// This update function updates search indexes for the change from
// https://www.drupal.org/node/2491175.
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('search_api.index.') as $index_id) {
$index = $config_factory->getEditable($index_id);
$processors = $index->get('processor_settings');
if (isset($processors['node_status'])) {
$processors['entity_status'] = $processors['node_status'];
unset($processors['node_status']);
$index->set('processor_settings', $processors);
// Mark the resulting configuration as trusted data. This avoids issues
// with future schema changes.
$index->save(TRUE);
}
}
// Clear the processor plugin cache so that if anything else indirectly tries
// to update Search API-related configuration, the plugin helper gets the most
// up-to-date plugin definitions.
\Drupal::getContainer()
->get('plugin.manager.search_api.processor')
->clearCachedDefinitions();
return t('Switched from old "Node status" to new "Entity status" processor.');
}
/**
* Update Views to use the time-based cache plugin for Search API.
*/
function search_api_update_8104() {
$config_factory = \Drupal::configFactory();
$changed = [];
foreach ($config_factory->listAll('views.view.') as $view_config_name) {
$view = $config_factory->getEditable($view_config_name);
$displays = $view->get('display');
$updated = FALSE;
foreach ($displays as $id => $display) {
if (!empty($display['display_options']['cache']['type']) && $display['display_options']['cache']['type'] === 'search_api') {
$displays[$id]['display_options']['cache']['type'] = 'search_api_time';
$updated = TRUE;
}
}
if ($updated) {
$view->set('display', $displays);
// Mark the resulting configuration as trusted data. This avoids issues
// with future schema changes.
$view->save(TRUE);
$changed[] = $view->get('id');
}
}
if (!empty($changed)) {
return \Drupal::translation()->translate('The following views have been updated to use the time-based cache plugin: @ids', ['@ids' => implode(', ', array_unique($changed))]);
}
return NULL;
}
@@ -0,0 +1,13 @@
drupal.search_api.admin_css:
version: VERSION
css:
theme:
css/search_api.admin.css: {}
drupal.search_api.processors:
version: VERSION
js:
js/search_api.processors.js: {}
dependencies:
- core/drupal
- core/jquery
- core/jquery.once
@@ -0,0 +1,15 @@
entity.search_api_server.add_form:
route_name: entity.search_api_server.add_form
title: 'Add server'
appears_on:
- search_api.overview
entity.search_api_index.add_form:
route_name: entity.search_api_index.add_form
title: 'Add index'
appears_on:
- search_api.overview
search_api.execute_tasks:
route_name: search_api.execute_tasks
title: 'Execute pending tasks'
appears_on:
- search_api.overview
@@ -0,0 +1,5 @@
search_api.overview:
title: Search API
description: 'Create and configure search indexes and servers.'
route_name: search_api.overview
parent: system.admin_config_search
@@ -0,0 +1,27 @@
entity.search_api_server.canonical:
route_name: entity.search_api_server.canonical
base_route: entity.search_api_server.canonical
title: 'View'
entity.search_api_server.edit_form:
route_name: entity.search_api_server.edit_form
base_route: entity.search_api_server.canonical
title: 'Edit'
entity.search_api_index.canonical:
route_name: entity.search_api_index.canonical
base_route: entity.search_api_index.canonical
title: 'View'
entity.search_api_index.edit_form:
route_name: entity.search_api_index.edit_form
base_route: entity.search_api_index.canonical
title: 'Edit'
weight: 5
entity.search_api_index.fields:
title: 'Fields'
route_name: entity.search_api_index.fields
base_route: entity.search_api_index.canonical
weight: 10
entity.search_api_index.processors:
route_name: entity.search_api_index.processors
base_route: entity.search_api_index.canonical
title: 'Processors'
weight: 20
@@ -0,0 +1,671 @@
<?php
/**
* @file
* Provides a rich framework for creating searches.
*/
use Drupal\comment\Entity\Comment;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Plugin\search_api\datasource\ContentEntity;
use Drupal\search_api\Plugin\search_api\datasource\ContentEntityTaskManager;
use Drupal\search_api\Plugin\search_api\datasource\EntityDatasourceInterface;
use Drupal\search_api\Plugin\views\argument\SearchApiTerm as TermArgument;
use Drupal\search_api\Plugin\views\filter\SearchApiTerm as TermFilter;
use Drupal\search_api\Plugin\views\query\SearchApiQuery;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Task\IndexTaskManager;
use Drupal\views\ViewEntityInterface;
/**
* Implements hook_help().
*/
function search_api_help($route_name) {
switch ($route_name) {
case 'search_api.overview':
$message = t('Below is a list of indexes grouped by the server they are associated with. A server is the definition of the actual indexing, querying and storage engine (for example, an Apache Solr server, the database, …). An index defines the indexed content (for example, all content and all comments on "Article" posts).');
$search_module_warning = _search_api_search_module_warning();
if ($search_module_warning) {
$message = "<p>$message</p><p>$search_module_warning</p>";
}
return $message;
}
return NULL;
}
/**
* Implements hook_cron().
*
* This will first execute pending tasks (if there are any). After that, items
* will be indexed on all enabled indexes with a non-zero cron limit. Indexing
* will run for the time set in the cron_worker_runtime config setting
* (defaulting to 15 seconds), but will at least index one batch of items on
* each index.
*/
function search_api_cron() {
// Execute pending server tasks.
\Drupal::getContainer()->get('search_api.server_task_manager')->execute();
// Load all enabled, not read-only indexes.
$conditions = [
'status' => TRUE,
];
$index_storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
/** @var \Drupal\search_api\IndexInterface[] $indexes */
$indexes = $index_storage->loadByProperties($conditions);
if (!$indexes) {
return;
}
// Add items to the tracking system for all indexes for which this hasn't
// happened yet.
$task_manager = \Drupal::getContainer()->get('search_api.task_manager');
foreach ($indexes as $index_id => $index) {
$conditions = [
'type' => IndexTaskManager::TRACK_ITEMS_TASK_TYPE,
'index_id' => $index_id,
];
$task_manager->executeSingleTask($conditions);
// Filter out read-only indexes here, since we want to have tracking but not
// index items for them.
if ($index->isReadOnly()) {
unset($indexes[$index_id]);
}
}
// Now index items.
// Remember servers which threw an exception.
$ignored_servers = [];
// Continue indexing, one batch from each index, until the time is up, but at
// least index one batch per index.
$settings = \Drupal::config('search_api.settings');
$default_cron_limit = $settings->get('default_cron_limit');
$end = time() + $settings->get('cron_worker_runtime');
$first_pass = TRUE;
while (TRUE) {
if (!$indexes) {
break;
}
foreach ($indexes as $id => $index) {
if (!$first_pass && time() >= $end) {
break 2;
}
if (!empty($ignored_servers[$index->getServerId()])) {
continue;
}
$limit = $index->getOption('cron_limit', $default_cron_limit);
$num = 0;
if ($limit) {
try {
$num = $index->indexItems($limit);
if ($num) {
$variables = [
'@num' => $num,
'%name' => $index->label(),
];
\Drupal::service('logger.channel.search_api')->info('Indexed @num items for index %name.', $variables);
}
}
catch (SearchApiException $e) {
// Exceptions will probably be caused by the server in most cases.
// Therefore, don't index for any index on this server.
$ignored_servers[$index->getServerId()] = TRUE;
$vars['%index'] = $index->label();
watchdog_exception('search_api', $e, '%type while trying to index items on %index: @message in %function (line %line of %file).', $vars);
}
}
if (!$num) {
// Couldn't index any items => stop indexing for this index in this
// cron run.
unset($indexes[$id]);
}
}
$first_pass = FALSE;
}
}
/**
* Implements hook_hook_info().
*/
function search_api_hook_info() {
$hooks = [
'search_api_backend_info_alter',
'search_api_server_features_alter',
'search_api_datasource_info_alter',
'search_api_processor_info_alter',
'search_api_data_type_info_alter',
'search_api_parse_mode_info_alter',
'search_api_tracker_info_alter',
'search_api_displays_alter',
'search_api_field_type_mapping_alter',
'search_api_views_handler_mapping_alter',
'search_api_views_field_handler_mapping_alter',
'search_api_index_items_alter',
'search_api_items_indexed',
'search_api_query_alter',
// Unfortunately, it's not possible to add hook infos for hooks with
// "wildcards".
// 'search_api_query_TAG_alter',
'search_api_results_alter',
// 'search_api_results_TAG_alter',
'search_api_index_reindex',
];
$info = [
'group' => 'search_api',
];
return array_fill_keys($hooks, $info);
}
/**
* Implements hook_config_import_steps_alter().
*/
function search_api_config_import_steps_alter(&$sync_steps, ConfigImporter $config_importer) {
$new = $config_importer->getUnprocessedConfiguration('create');
$changed = $config_importer->getUnprocessedConfiguration('update');
$new_or_changed = array_merge($new, $changed);
$prefix = \Drupal::entityTypeManager()->getDefinition('search_api_index')->getConfigPrefix() . '.';
$prefix_length = strlen($prefix);
foreach ($new_or_changed as $config_id) {
if (substr($config_id, 0, $prefix_length) === $prefix) {
$sync_steps[] = ['Drupal\search_api\Task\IndexTaskManager', 'processIndexTasks'];
}
}
}
/**
* Implements hook_entity_insert().
*
* Adds entries for all languages of the new entity to the tracking table for
* each index that tracks entities of this type.
*
* By setting the $entity->search_api_skip_tracking property to a true-like
* value before this hook is invoked, you can prevent this behavior and make the
* Search API ignore this new entity.
*
* Note that this function implements tracking only on behalf of the "Content
* Entity" datasource defined in this module, not for entity-based datasources
* in general. Datasources defined by other modules still have to implement
* their own mechanism for tracking new/updated/deleted entities.
*
* @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
*/
function search_api_entity_insert(EntityInterface $entity) {
// Check if the entity is a content entity.
if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) {
return;
}
$indexes = ContentEntity::getIndexesForEntity($entity);
if (!$indexes) {
return;
}
// Compute the item IDs for all languages of the entity.
$item_ids = [];
$entity_id = $entity->id();
foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
$item_ids[] = $entity_id . ':' . $langcode;
}
$datasource_id = 'entity:' . $entity->getEntityTypeId();
foreach ($indexes as $index) {
$filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $item_ids);
$index->trackItemsInserted($datasource_id, $filtered_item_ids);
}
}
/**
* Implements hook_entity_update().
*
* Updates the corresponding tracking table entries for each index that tracks
* this entity.
*
* Also takes care of new or deleted translations.
*
* By setting the $entity->search_api_skip_tracking property to a true-like
* value before this hook is invoked, you can prevent this behavior and make the
* Search API ignore this update.
*
* Note that this function implements tracking only on behalf of the "Content
* Entity" datasource defined in this module, not for entity-based datasources
* in general. Datasources defined by other modules still have to implement
* their own mechanism for tracking new/updated/deleted entities.
*
* @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
*/
function search_api_entity_update(EntityInterface $entity) {
// Check if the entity is a content entity.
if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) {
return;
}
$indexes = ContentEntity::getIndexesForEntity($entity);
if (!$indexes) {
return;
}
// Compare old and new languages for the entity to identify inserted,
// updated and deleted translations (and, therefore, search items).
$entity_id = $entity->id();
$inserted_item_ids = [];
$updated_item_ids = $entity->getTranslationLanguages();
$deleted_item_ids = [];
$old_translations = $entity->original->getTranslationLanguages();
foreach ($old_translations as $langcode => $language) {
if (!isset($updated_item_ids[$langcode])) {
$deleted_item_ids[] = $langcode;
}
}
foreach ($updated_item_ids as $langcode => $language) {
if (!isset($old_translations[$langcode])) {
unset($updated_item_ids[$langcode]);
$inserted_item_ids[] = $langcode;
}
}
$datasource_id = 'entity:' . $entity->getEntityTypeId();
$combine_id = function ($langcode) use ($entity_id) {
return $entity_id . ':' . $langcode;
};
$inserted_item_ids = array_map($combine_id, $inserted_item_ids);
$updated_item_ids = array_map($combine_id, array_keys($updated_item_ids));
$deleted_item_ids = array_map($combine_id, $deleted_item_ids);
foreach ($indexes as $index) {
if ($inserted_item_ids) {
$filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $inserted_item_ids);
$index->trackItemsInserted($datasource_id, $filtered_item_ids);
}
if ($updated_item_ids) {
$index->trackItemsUpdated($datasource_id, $updated_item_ids);
}
if ($deleted_item_ids) {
$index->trackItemsDeleted($datasource_id, $deleted_item_ids);
}
}
}
/**
* Implements hook_entity_delete().
*
* Deletes all entries for this entity from the tracking table for each index
* that tracks this entity type.
*
* By setting the $entity->search_api_skip_tracking property to a true-like
* value before this hook is invoked, you can prevent this behavior and make the
* Search API ignore this deletion. (Note that this might lead to stale data in
* the tracking table or on the server, since the item will not removed from
* there (if it has been added before).)
*
* Note that this function implements tracking only on behalf of the "Content
* Entity" datasource defined in this module, not for entity-based datasources
* in general. Datasources defined by other modules still have to implement
* their own mechanism for tracking new/updated/deleted entities.
*
* @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
*/
function search_api_entity_delete(EntityInterface $entity) {
// Check if the entity is a content entity.
if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) {
return;
}
$indexes = ContentEntity::getIndexesForEntity($entity);
if (!$indexes) {
return;
}
// Remove the search items for all the entity's translations.
$item_ids = [];
$entity_id = $entity->id();
foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
$item_ids[] = $entity_id . ':' . $langcode;
}
$datasource_id = 'entity:' . $entity->getEntityTypeId();
foreach ($indexes as $index) {
$index->trackItemsDeleted($datasource_id, $item_ids);
}
}
/**
* Implements hook_node_access_records_alter().
*
* Marks the node and its comments changed for indexes that use the "Content
* access" processor.
*/
function search_api_node_access_records_alter(array &$grants, NodeInterface $node) {
/** @var \Drupal\search_api\IndexInterface $index */
foreach (Index::loadMultiple() as $index) {
if (!$index->hasValidTracker() || !$index->status()) {
continue;
}
if (!$index->isValidProcessor('content_access')) {
continue;
}
foreach ($index->getDatasources() as $datasource_id => $datasource) {
switch ($datasource->getEntityTypeId()) {
case 'node':
// Don't index the node if search_api_skip_tracking is set on it.
if ($node->search_api_skip_tracking) {
continue 2;
}
$item_id = $datasource->getItemId($node->getTypedData());
if ($item_id !== NULL) {
$index->trackItemsUpdated($datasource_id, [$item_id]);
}
break;
case 'comment':
if (!isset($comments)) {
$entity_query = \Drupal::entityQuery('comment');
$entity_query->condition('entity_id', (int) $node->id());
$entity_query->condition('entity_type', 'node');
$comment_ids = $entity_query->execute();
/** @var \Drupal\comment\CommentInterface[] $comments */
$comments = Comment::loadMultiple($comment_ids);
}
$item_ids = [];
foreach ($comments as $comment) {
$item_id = $datasource->getItemId($comment->getTypedData());
if ($item_id !== NULL) {
$item_ids[] = $item_id;
}
}
if ($item_ids) {
$index->trackItemsUpdated($datasource_id, $item_ids);
}
break;
}
}
}
}
/**
* Implements hook_theme().
*/
function search_api_theme() {
return [
'search_api_admin_fields_table' => [
'render element' => 'element',
'function' => 'theme_search_api_admin_fields_table',
'file' => 'search_api.theme.inc',
],
'search_api_admin_data_type_table' => [
'variables' => [
'data_types' => [],
'fallback_mapping' => []
],
'function' => 'theme_search_api_admin_data_type_table',
'file' => 'search_api.theme.inc',
],
'search_api_form_item_list' => [
'render element' => 'element',
'function' => 'theme_search_api_form_item_list',
'file' => 'search_api.theme.inc',
],
'search_api_server' => [
'variables' => ['server' => NULL],
'function' => 'theme_search_api_server',
'file' => 'search_api.theme.inc',
],
'search_api_index' => [
'variables' => [
'index' => NULL,
'server_count' => NULL,
'server_count_error' => NULL,
],
'function' => 'theme_search_api_index',
'file' => 'search_api.theme.inc',
],
];
}
/**
* Implements hook_ENTITY_TYPE_update() for type "search_api_index".
*
* Implemented on behalf of the "entity" datasource plugin.
*
* @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
*/
function search_api_search_api_index_update(IndexInterface $index) {
if (!$index->status() || empty($index->original)) {
return;
}
/** @var \Drupal\search_api\IndexInterface $original */
$original = $index->original;
if (!$original->status()) {
return;
}
foreach ($index->getDatasources() as $datasource_id => $datasource) {
if ($datasource->getBaseId() != 'entity'
|| !($datasource instanceof EntityDatasourceInterface)
|| !$original->isValidDatasource($datasource_id)) {
continue;
}
$old_datasource = $original->getDatasource($datasource_id);
$old_config = $old_datasource->getConfiguration();
$new_config = $datasource->getConfiguration();
if ($old_config != $new_config) {
// Bundles and languages share the same structure, so changes can be
// processed in a unified way.
$tasks = [];
$insert_task = ContentEntityTaskManager::INSERT_ITEMS_TASK_TYPE;
$delete_task = ContentEntityTaskManager::DELETE_ITEMS_TASK_TYPE;
$settings = [];
$entity_type = \Drupal::entityTypeManager()
->getDefinition($datasource->getEntityTypeId());
if ($entity_type->hasKey('bundle')) {
$settings['bundles'] = $datasource->getBundles();
}
if ($entity_type->isTranslatable()) {
$settings['languages'] = \Drupal::languageManager()->getLanguages();
}
// Determine which bundles/languages have been newly selected or
// deselected and then assign them to the appropriate actions depending
// on the current "default" setting.
foreach ($settings as $setting => $all) {
$old_selected = array_flip($old_config[$setting]['selected']);
$new_selected = array_flip($new_config[$setting]['selected']);
// First, check if the "default" setting changed and invert the checked
// items for the old config, so the following comparison makes sense.
if ($old_config[$setting]['default'] != $new_config[$setting]['default']) {
$old_selected = array_diff_key($all, $old_selected);
}
$newly_selected = array_keys(array_diff_key($new_selected, $old_selected));
$newly_unselected = array_keys(array_diff_key($old_selected, $new_selected));
if ($new_config[$setting]['default']) {
$tasks[$insert_task][$setting] = $newly_unselected;
$tasks[$delete_task][$setting] = $newly_selected;
}
else {
$tasks[$insert_task][$setting] = $newly_selected;
$tasks[$delete_task][$setting] = $newly_unselected;
}
}
// This will keep only those tasks where at least one of "bundles" or
// "languages" is non-empty.
$tasks = array_filter($tasks, 'array_filter');
$task_manager = \Drupal::getContainer()
->get('search_api.task_manager');
foreach ($tasks as $task => $data) {
$data += [
'datasource' => $datasource_id,
'page' => 0,
];
$task_manager->addTask($task, NULL, $index, $data);
}
// If we added any new tasks, set a batch for them. (If we aren't in a
// form submission, this will just be ignored.)
if ($tasks) {
$task_manager->setTasksBatch([
'index_id' => $index->id(),
'type' => array_keys($tasks),
]);
}
}
}
}
/**
* Implements hook_views_plugins_argument_alter().
*/
function search_api_views_plugins_argument_alter(array &$plugins) {
// We have to include the term argument handler like this, since adding it
// directly (i.e., with an annotation) would cause fatal errors on sites
// without the Taxonomy module.
if (\Drupal::moduleHandler()->moduleExists('taxonomy')) {
$plugins['search_api_term'] = [
'plugin_type' => 'argument',
'id' => 'search_api_term',
'class' => TermArgument::class,
'provider' => 'search_api',
];
}
}
/**
* Implements hook_views_plugins_filter_alter().
*/
function search_api_views_plugins_filter_alter(array &$plugins) {
// We have to include the term filter handler like this, since adding it
// directly (i.e., with an annotation) would cause fatal errors on sites
// without the Taxonomy module.
if (\Drupal::moduleHandler()->moduleExists('taxonomy')) {
$plugins['search_api_term'] = [
'plugin_type' => 'filter',
'id' => 'search_api_term',
'class' => TermFilter::class,
'provider' => 'search_api',
];
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for type "view".
*/
function search_api_view_insert(ViewEntityInterface $view) {
_search_api_view_crud_event($view);
// Disable Views' default caching mechanisms on Search API views.
$displays = $view->get('display');
if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
$change = FALSE;
foreach ($displays as $id => $display) {
if (isset($display['display_options']['cache']['type']) && in_array($display['display_options']['cache']['type'], ['tag', 'time'])) {
$displays[$id]['display_options']['cache']['type'] = 'none';
$change = TRUE;
}
}
if ($change) {
drupal_set_message(\Drupal::translation()->translate('The selected caching mechanism does not work with views on Search API indexes. Please either use one of the Search API-specific caching options or "None". Caching was turned off for this view.'), 'warning');
$view->set('display', $displays);
$view->save();
}
}
}
/**
* Implements hook_ENTITY_TYPE_update() for type "view".
*/
function search_api_view_update(ViewEntityInterface $view) {
_search_api_view_crud_event($view);
}
/**
* Implements hook_ENTITY_TYPE_delete() for type "view".
*/
function search_api_view_delete(ViewEntityInterface $view) {
_search_api_view_crud_event($view);
}
/**
* Reacts to a view CRUD event.
*
* @param \Drupal\views\ViewEntityInterface $view
* The view that was created, changed or deleted.
*/
function _search_api_view_crud_event(ViewEntityInterface $view) {
// Whenever a view is created, updated (displays might have been added or
// removed) or deleted, we need to clear our cached display definitions.
if (SearchApiQuery::getIndexFromTable($view->get('base_table'))) {
\Drupal::getContainer()
->get('plugin.manager.search_api.display')
->clearCachedDefinitions();
}
}
/**
* Retrieves the allowed values for a list field instance.
*
* @param string $entity_type
* The entity type to which the field is attached.
* @param string $bundle
* The bundle to which the field is attached.
* @param string $field_name
* The field's field name.
*
* @return array|null
* An array of allowed values in the form key => label, or NULL.
*
* @see _search_api_views_handler_adjustments()
*/
function _search_api_views_get_allowed_values($entity_type, $bundle, $field_name) {
$field_manager = \Drupal::getContainer()->get('entity_field.manager');
$field_definitions = $field_manager->getFieldDefinitions($entity_type, $bundle);
if (!empty($field_definitions[$field_name])) {
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
$field_definition = $field_definitions[$field_name];
$options = $field_definition->getSetting('allowed_values');
if ($options) {
return $options;
}
}
return NULL;
}
/**
* Implements hook_form_FORM_ID_alter() for form "views_ui_edit_display_form".
*/
function search_api_form_views_ui_edit_display_form_alter(&$form, FormStateInterface $form_state) {
// Disable Views' default caching mechanisms on Search API views.
$displays = $form_state->getStorage()['view']->get('display');
if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
unset($form['options']['cache']['type']['#options']['tag']);
unset($form['options']['cache']['type']['#options']['time']);
}
}
/**
* Returns a warning message if the Core Search module is enabled.
*
* @return string|null
* A warning message if needed, NULL otherwise.
*
* @see search_api_install()
* @see search_api_requirements()
*/
function _search_api_search_module_warning() {
if (\Drupal::moduleHandler()->moduleExists('search')) {
$args = [
':url' => Url::fromRoute('system.modules_uninstall')->toString(),
':documentation' => 'https://www.drupal.org/node/2010146#core-search',
];
return t('The default Drupal core Search module is still enabled. If you are using Search API, you probably want to <a href=":url">uninstall</a> the Search module for performance reasons. For more information see <a href=":documentation">the Search API handbook</a>.', $args);
}
return NULL;
}
@@ -0,0 +1,3 @@
'administer search_api':
title: 'Administer Search API'
description: 'Create and configure Search API servers and indexes.'
@@ -0,0 +1,34 @@
search_api_backend:
label: Search API backend
plugin_manager_service_id: plugin.manager.search_api.backend
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
search_api_datasource:
label: Search API datasource
plugin_manager_service_id: plugin.manager.search_api.datasource
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
search_api_data_type:
label: Search API data type
plugin_manager_service_id: plugin.manager.search_api.data_type
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
search_api_display:
label: Search API display
plugin_manager_service_id: plugin.manager.search_api.display
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
search_api_processor:
label: Search API processor
plugin_manager_service_id: plugin.manager.search_api.processor
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
search_api_parse_mode:
label: Search API parse mode
plugin_manager_service_id: plugin.manager.search_api.parse_mode
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
search_api_tracker:
label: Search API tracker
plugin_manager_service_id: plugin.manager.search_api.tracker
plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
@@ -0,0 +1,242 @@
search_api.overview:
path: '/admin/config/search/search-api'
defaults:
_title: 'Search API'
_entity_list: 'search_api_index'
requirements:
_permission: 'administer search_api'
search_api.execute_tasks:
path: '/admin/config/search/search-api/execute-tasks'
defaults:
_controller: '\Drupal\search_api\Controller\TaskController::executeTasks'
_title: 'Execute pending tasks'
requirements:
_permission: 'administer search_api'
_search_api_tasks: 'TRUE'
entity.search_api_server.add_form:
path: '/admin/config/search/search-api/add-server'
defaults:
_entity_form: 'search_api_server.default'
requirements:
_entity_create_access: 'search_api_server'
entity.search_api_server.canonical:
path: '/admin/config/search/search-api/server/{search_api_server}'
defaults:
_controller: '\Drupal\search_api\Controller\ServerController::page'
_title_callback: '\Drupal\search_api\Controller\ServerController::pageTitle'
_title: "View"
requirements:
_entity_access: 'search_api_server.view'
options:
parameters:
search_api_server:
with_config_overrides: TRUE
entity.search_api_server.edit_form:
path: '/admin/config/search/search-api/server/{search_api_server}/edit'
defaults:
_entity_form: 'search_api_server.edit'
requirements:
_entity_access: 'search_api_server.edit'
entity.search_api_server.delete_form:
path: '/admin/config/search/search-api/server/{search_api_server}/delete'
defaults:
_entity_form: 'search_api_server.delete'
requirements:
_entity_access: 'search_api_server.delete'
options:
parameters:
search_api_server:
with_config_overrides: TRUE
entity.search_api_server.enable:
path: '/admin/config/search/search-api/server/{search_api_server}/enable'
defaults:
_controller: 'Drupal\search_api\Controller\ServerController::serverBypassEnable'
requirements:
_entity_access: 'search_api_server.enable'
_csrf_token: 'TRUE'
entity.search_api_server.disable:
path: '/admin/config/search/search-api/server/{search_api_server}/disable'
defaults:
_entity_form: 'search_api_server.disable'
requirements:
_entity_access: 'search_api_server.disable'
entity.search_api_server.clear:
path: '/admin/config/search/search-api/server/{search_api_server}/clear'
defaults:
_entity_form: 'search_api_server.clear'
requirements:
_entity_access: 'search_api_server.clear'
options:
parameters:
search_api_server:
with_config_overrides: TRUE
entity.search_api_index.add_form:
path: '/admin/config/search/search-api/add-index'
defaults:
_entity_form: 'search_api_index.default'
requirements:
_entity_create_access: 'search_api_index'
entity.search_api_index.canonical:
path: '/admin/config/search/search-api/index/{search_api_index}'
defaults:
_controller: '\Drupal\search_api\Controller\IndexController::page'
_title_callback: '\Drupal\search_api\Controller\IndexController::pageTitle'
requirements:
_entity_access: 'search_api_index.view'
options:
parameters:
search_api_index:
with_config_overrides: TRUE
entity.search_api_index.edit_form:
path: '/admin/config/search/search-api/index/{search_api_index}/edit'
defaults:
_entity_form: 'search_api_index.edit'
requirements:
_entity_access: 'search_api_index.edit'
entity.search_api_index.delete_form:
path: '/admin/config/search/search-api/index/{search_api_index}/delete'
defaults:
_entity_form: 'search_api_index.delete'
requirements:
_entity_access: 'search_api_index.delete'
options:
parameters:
search_api_index:
with_config_overrides: TRUE
entity.search_api_index.enable:
path: '/admin/config/search/search-api/index/{search_api_index}/enable'
defaults:
_controller: 'Drupal\search_api\Controller\IndexController::indexBypassEnable'
requirements:
_entity_access: 'search_api_index.enable'
_csrf_token: 'TRUE'
entity.search_api_index.disable:
path: '/admin/config/search/search-api/index/{search_api_index}/disable'
defaults:
_entity_form: 'search_api_index.disable'
requirements:
_entity_access: 'search_api_index.disable'
entity.search_api_index.fields:
path: '/admin/config/search/search-api/index/{search_api_index}/fields'
options:
parameters:
search_api_index:
tempstore: TRUE
type: 'entity:search_api_index'
defaults:
_entity_form: 'search_api_index.fields'
requirements:
_entity_access: 'search_api_index.fields'
entity.search_api_index.add_fields:
path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/nojs'
options:
parameters:
search_api_index:
tempstore: TRUE
type: 'entity:search_api_index'
defaults:
_entity_form: 'search_api_index.add_fields'
requirements:
_entity_access: 'search_api_index.fields'
entity.search_api_index.add_fields_ajax:
path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/ajax'
options:
parameters:
search_api_index:
tempstore: TRUE
type: 'entity:search_api_index'
defaults:
_entity_form: 'search_api_index.add_fields'
requirements:
_entity_access: 'search_api_index.fields'
entity.search_api_index.field_config:
path: '/admin/config/search/search-api/index/{search_api_index}/fields/edit/{field_id}'
options:
parameters:
search_api_index:
tempstore: TRUE
type: 'entity:search_api_index'
defaults:
_title: 'Edit field'
_entity_form: 'search_api_index.field_config'
requirements:
_entity_access: 'search_api_index.fields'
entity.search_api_index.remove_field:
path: '/admin/config/search/search-api/index/{search_api_index}/fields/remove/{field_id}'
options:
parameters:
search_api_index:
tempstore: TRUE
type: 'entity:search_api_index'
defaults:
_controller: 'Drupal\search_api\Controller\IndexController::removeField'
requirements:
_entity_access: 'search_api_index.fields'
_csrf_token: 'TRUE'
entity.search_api_index.break_lock_form:
path: '/admin/config/search/search-api/index/{search_api_index}/fields/break-lock'
defaults:
_entity_form: 'search_api_index.break_lock'
_title: 'Break lock'
requirements:
_entity_access: 'search_api_index.break-lock'
entity.search_api_index.processors:
path: '/admin/config/search/search-api/index/{search_api_index}/processors'
defaults:
_entity_form: 'search_api_index.processors'
requirements:
_entity_access: 'search_api_index.processors'
entity.search_api_index.reindex:
path: '/admin/config/search/search-api/index/{search_api_index}/reindex'
defaults:
_entity_form: 'search_api_index.reindex'
requirements:
_entity_access: 'search_api_index.reindex'
options:
parameters:
search_api_index:
with_config_overrides: TRUE
entity.search_api_index.clear:
path: '/admin/config/search/search-api/index/{search_api_index}/clear'
defaults:
_entity_form: 'search_api_index.clear'
requirements:
_entity_access: 'search_api_index.clear'
options:
parameters:
search_api_index:
with_config_overrides: TRUE
entity.search_api_index.rebuild_tracker:
path: '/admin/config/search/search-api/index/{search_api_index}/rebuild-tracker'
defaults:
_entity_form: 'search_api_index.rebuild_tracker'
requirements:
_entity_access: 'search_api_index.rebuild_tracker'
options:
parameters:
search_api_index:
with_config_overrides: TRUE
@@ -0,0 +1,94 @@
services:
access_check.search_api_tasks:
class: Drupal\search_api\Controller\ExecuteTasksAccessCheck
arguments: ['@search_api.task_manager']
tags:
- { name: access_check, applies_to: _search_api_tasks }
logger.channel.search_api:
parent: logger.channel_base
arguments: ['search_api']
paramconverter.search_api:
class: Drupal\search_api\ParamConverter\SearchApiConverter
arguments: ['@entity.manager', '@user.shared_tempstore', '@current_user']
tags:
- { name: paramconverter, priority: 10 }
lazy: true
plugin.manager.search_api.backend:
class: Drupal\search_api\Backend\BackendPluginManager
parent: default_plugin_manager
plugin.manager.search_api.data_type:
class: Drupal\search_api\DataType\DataTypePluginManager
parent: default_plugin_manager
plugin.manager.search_api.datasource:
class: Drupal\search_api\Datasource\DatasourcePluginManager
parent: default_plugin_manager
plugin.manager.search_api.display:
class: Drupal\search_api\Display\DisplayPluginManager
parent: default_plugin_manager
plugin.manager.search_api.parse_mode:
class: Drupal\search_api\ParseMode\ParseModePluginManager
parent: default_plugin_manager
plugin.manager.search_api.processor:
class: Drupal\search_api\Processor\ProcessorPluginManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@string_translation']
plugin.manager.search_api.tracker:
class: Drupal\search_api\Tracker\TrackerPluginManager
parent: default_plugin_manager
search_api.datasource_task_manager:
class: Drupal\search_api\Plugin\search_api\datasource\ContentEntityTaskManager
arguments: ['@search_api.task_manager', '@entity_type.manager']
tags:
- { name: event_subscriber }
search_api.data_type_helper:
class: \Drupal\search_api\Utility\DataTypeHelper
arguments: ['@module_handler', '@plugin.manager.search_api.data_type']
search_api.fields_helper:
class: \Drupal\search_api\Utility\FieldsHelper
arguments: ['@entity_type.manager', '@entity_field.manager', '@entity_type.bundle.info', '@search_api.data_type_helper']
search_api.index_task_manager:
class: Drupal\search_api\Task\IndexTaskManager
arguments: ['@search_api.task_manager', '@entity_type.manager']
tags:
- { name: event_subscriber }
search_api.plugin_helper:
class: Drupal\search_api\Utility\PluginHelper
arguments: ['@plugin.manager.search_api.datasource', '@plugin.manager.search_api.processor', '@plugin.manager.search_api.tracker']
search_api.post_request_indexing:
class: Drupal\search_api\Utility\PostRequestIndexing
arguments: ['@entity_type.manager']
tags:
- { name: event_subscriber }
search_api.query_helper:
class: Drupal\search_api\Utility\QueryHelper
arguments: ['@request_stack', '@module_handler', '@plugin.manager.search_api.parse_mode']
search_api.server_task_manager:
class: Drupal\search_api\Task\ServerTaskManager
arguments: ['@search_api.task_manager', '@entity_type.manager']
tags:
- { name: event_subscriber }
search_api.task_manager:
class: Drupal\search_api\Task\TaskManager
arguments: ['@entity_type.manager', '@event_dispatcher', '@string_translation']
search_api.vbo_view_data_provider:
class: Drupal\search_api\Contrib\ViewsBulkOperationsEventSubscriber
tags:
- { name: event_subscriber }
@@ -0,0 +1,506 @@
<?php
/**
* @file
* Defines theme functions for the Search API module.
*/
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\Utility;
/**
* Returns HTML for a fields form table.
*
* @param array $variables
* An associative array containing:
* - element: A render element representing the form.
*
* @return string
* The rendered HTML for a fields form table.
*
* @ingroup themeable
*/
function theme_search_api_admin_fields_table(array $variables) {
$form = $variables['element'];
$rows = [];
if (!empty($form['fields'])) {
foreach (Element::children($form['fields']) as $name) {
$row = [];
foreach (Element::children($form['fields'][$name]) as $field) {
if ($cell = render($form['fields'][$name][$field])) {
$row[] = $cell;
}
}
$row = [
'data' => $row,
'data-field-row-id' => $name,
];
if (!empty($form['fields'][$name]['description']['#value'])) {
$row['title'] = strip_tags($form['fields'][$name]['description']['#value']);
}
$rows[] = $row;
}
}
$note = isset($form['note']) ? $form['note'] : '';
unset($form['note'], $form['submit']);
$output = '';
foreach (Element::children($form) as $key) {
if (!empty($form[$key])) {
$output .= render($form[$key]);
}
}
$build = [
'#theme' => 'table',
'#header' => $form['#header'],
'#rows' => $rows,
'#empty' => t('No fields have been added for this datasource.'),
];
$output .= render($build);
$output .= render($note);
return $output;
}
/**
* Returns HTML for the data type overview table.
*
* @param array $variables
* An associative array containing:
* - data_types: An associative array of data types, keyed by data type ID and
* containing associative arrays with information about the data type:
* - label: The (translated) human-readable label for the data type.
* - description: The (translated) description of the data type.
* - fallback: The type ID of the fallback type.
* - fallback_mapping: array of fallback data types for unsupported data
* types.
*
* @return string
* The rendered HTML for a fields form table.
*
* @ingroup themeable
*/
function theme_search_api_admin_data_type_table(array $variables) {
$data_types = $variables['data_types'];
$fallback_mapping = $variables['fallback_mapping'];
$header = [
t('Data Type'),
t('Description'),
t('Supported'),
];
// Only show the column with fallback types if there is actually an
// unsupported type listed.
if ($fallback_mapping) {
$header[] = t('Fallback data type');
}
$rows = [];
$yes = t('Yes');
$yes_img = 'core/misc/icons/73b355/check.svg';
$no = t('No');
$no_img = 'core/misc/icons/e32700/error.svg';
foreach ($data_types as $data_type_id => $data_type) {
$has_fallback = isset($fallback_mapping[$data_type_id]);
$supported_label = $has_fallback ? $no : $yes;
$supported_icon = [
'#theme' => 'image',
'#uri' => $has_fallback ? $no_img : $yes_img,
'#width' => 18,
'#height' => 18,
'#alt' => $supported_label,
'#title' => $supported_label,
];
$row = [
$data_type['label'],
$data_type['description'],
['data' => $supported_icon],
];
if ($fallback_mapping) {
$row[] = $has_fallback ? $data_types[$data_type['fallback']]['label'] : '';
}
$rows[] = $row;
}
$build = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
];
return render($build);
}
/**
* Returns HTML for a list of form items.
*
* Wrapper around the "item_list" theme which uses the child elements as the
* list items instead of the "#items" key.
*
* @param array $variables
* An associative array containing a single value, "element", containing the
* element to be rendered.
*
* @return string
* The rendered HTML for the list.
*
* @ingroup themeable
*/
function theme_search_api_form_item_list(array $variables) {
$element = $variables['element'];
$build = [
'#theme' => 'item_list',
];
if (!empty($element['#title'])) {
$build['#title'] = $element['#title'];
}
foreach (Element::children($element) as $key) {
$build['#items'][$key] = $element[$key];
}
return render($build);
}
/**
* Returns HTML for a search server.
*
* @param array $variables
* An associative array containing:
* - server: The server that should be displayed.
*
* @return string
* The rendered HTML for a search server.
*
* @ingroup themeable
*/
function theme_search_api_server(array $variables) {
// Get the search server.
/** @var \Drupal\search_api\ServerInterface $server */
$server = $variables['server'];
$output = '';
if (($description = $server->getDescription())) {
// Sanitize the description and append to the output.
$output .= '<p class="description">' . nl2br(Html::escape($description)) . '</p>';
}
// Initialize the $rows variable which will hold the different parts of server
// information.
$rows = [];
// Create a row template with references so we don't have to deal with the
// complicated structure for each individual row.
$row = [
'data' => [
['header' => TRUE],
'',
],
'class' => [''],
];
// Get the individual parts of the row by reference.
$label = &$row['data'][0]['data'];
$info = &$row['data'][1];
$classes = &$row['class'];
// Check if the server is enabled.
if ($server->status()) {
$classes[] = 'ok';
$info = t('enabled (<a href=":url">disable</a>)', [':url' => $server->toUrl('disable')->toString()]);
}
else {
$classes[] = 'warning';
$info = t('disabled (<a href=":url">enable</a>)', [':url' => $server->toUrl('enable')->toString()]);
}
// Append the row and reset variables.
$label = t('Status');
$classes[] = 'search-api-server-summary--status';
$rows[] = Utility::deepCopy($row);
$classes = [];
// Check if the backend used by the server is valid and get its label.
if ($server->hasValidBackend()) {
$backend = $server->getBackend();
$info = Html::escape($backend->label());
}
else {
$classes[] = 'error';
$info = t('Invalid or missing backend plugin: %backend_id', ['%backend_id' => $server->getBackendId()]);
}
// Append the row and reset variables.
$label = t('Backend class');
$classes[] = 'search-api-server-summary--backend';
$rows[] = Utility::deepCopy($row);
$classes = [];
// Build the indexes links container.
$indexes = [
'#theme' => 'links',
'#attributes' => ['class' => ['inline']],
'#links' => [],
];
// Add links for all indexes attached to this server.
foreach ($server->getIndexes() as $index) {
$indexes['#links'][] = [
'title' => $index->label(),
'url' => $index->toUrl('canonical'),
];
}
// Check if the indexes variable contains links.
if ($indexes['#links']) {
$label = t('Search indexes');
$info = render($indexes);
$classes[] = 'search-api-server-summary--indexes';
$rows[] = Utility::deepCopy($row);
$classes = [];
}
// Add backend-specific additional information.
foreach ($server->viewSettings() as $information) {
// Convert the extra information and append the information to the row.
$label = $information['label'];
$info = $information['info'];
if (!empty($information['status'])) {
$classes[] = $information['status'];
}
$rows[] = Utility::deepCopy($row);
$classes = [];
}
// Append the server info table to the output.
$server_info_table = [
'#theme' => 'table',
'#rows' => $rows,
'#attributes' => [
'class' => [
'search-api-server-summary',
],
],
];
$output .= render($server_info_table);
return $output;
}
/**
* Implements hook_preprocess_search_api_index().
*/
function search_api_preprocess_search_api_index(array &$variables) {
/** @var \Drupal\search_api\IndexInterface $index */
$index = $variables['index'];
if ($index->status()) {
try {
$variables['server_count'] = $index->query()
->setProcessingLevel(QueryInterface::PROCESSING_NONE)
->addTag('server_index_status')
->range(0, 0)
->execute()
->getResultCount();
}
catch (SearchApiException $e) {
$variables['server_count_error'] = $e->getMessage();
}
}
}
/**
* Returns HTML for a search index.
*
* @param array $variables
* An associative array containing:
* - index: The search index to display.
* - server_count: The count of items on the server for this index.
* - server_count_error: If set, the error that occurred while trying to get
* the server items count.
*
* @return string
* The rendered HTML for a search index.
*
* @ingroup themeable
*/
function theme_search_api_index(array $variables) {
// Get the index.
/** @var \Drupal\search_api\IndexInterface $index */
$index = $variables['index'];
$server = $index->hasValidServer() ? $index->getServerInstance() : NULL;
$tracker = $index->hasValidTracker() ? $index->getTrackerInstance() : NULL;
$output = '';
if (($description = $index->getDescription())) {
// Sanitize the description and append to the output.
$output .= '<p class="description">' . nl2br(Html::escape($description)) . '</p>';
}
// Initialize the $rows variable which will hold the different parts of server
// information.
$rows = [];
// Create a row template with references so we don't have to deal with the
// complicated structure for each individual row.
$row = [
'data' => [
['header' => TRUE],
'',
],
'class' => [],
];
// Get the individual parts of the row by reference.
$label = &$row['data'][0]['data'];
$info = &$row['data'][1];
$classes = &$row['class'];
// Check if the index is enabled.
if ($index->status()) {
$classes[] = 'ok';
$info = t('enabled (<a href=":url">disable</a>)', [':url' => $index->toUrl('disable')->toString()]);
}
// Check if a server is available and enabled.
elseif ($server && $server->status()) {
$classes[] = 'warning';
$info = t('disabled (<a href=":url">enable</a>)', [':url' => $index->toUrl('enable')->toString()]);
}
else {
$classes[] = 'warning';
$info = t('disabled');
}
// Append the row and reset variables.
$label = t('Status');
$classes[] = 'search-api-index-summary--status';
$rows[] = Utility::deepCopy($row);
$classes = [];
foreach ($index->getDatasourceIds() as $datasource_id) {
// Check if the datasource is valid.
if ($index->isValidDatasource($datasource_id)) {
$info = $index->getDatasource($datasource_id)->label();
if ($tracker) {
$args = [
'@indexed' => $tracker->getIndexedItemsCount($datasource_id),
'@total' => $tracker->getTotalItemsCount($datasource_id),
];
$indexed = t('@indexed/@total indexed', $args);
$args = [
'@datasource' => $info,
'@indexed' => $indexed,
];
$info = new FormattableMarkup('@datasource <small>(@indexed)</small>', $args);
}
}
else {
$classes[] = 'error';
$info = t('Invalid or missing datasource plugin: %datasource_id', ['%datasource_id' => $datasource_id]);
}
// Append the row and reset variables.
$label = t('Datasource');
$classes[] = 'search-api-index-summary--datasource';
$rows[] = Utility::deepCopy($row);
$classes = [];
}
// Check if the tracker is valid.
if ($tracker) {
$info = $tracker->label();
}
else {
$classes[] = 'error';
$info = t('Invalid or missing tracker plugin: %tracker_id', ['%tracker_id' => $index->getTrackerId()]);
}
// Append the row and reset variables.
$label = t('Tracker');
$classes[] = 'search-api-index-summary--tracker';
$rows[] = Utility::deepCopy($row);
$classes = [];
// Check if a server is available.
$classes[] = 'search-api-index-summary--server';
if ($server) {
$label = t('Server');
$info = $server->toLink(NULL, 'canonical')->toString();
$rows[] = Utility::deepCopy($row);
}
elseif ($index->getServerId()) {
$classes[] = 'error';
$label = t('Server');
$info = t('Unknown server set for index: %server_id', ['%server_id' => $index->getServerId()]);
$rows[] = Utility::deepCopy($row);
}
$classes = [];
// Check if the index is enabled.
if ($index->status()) {
$label = t('Server index status');
if (isset($variables['server_count'])) {
$vars = [':url' => Url::fromUri('https://drupal.org/node/2009804#server-index-status')->toString()];
// Build the server index status info.
$info = \Drupal::translation()->formatPlural($variables['server_count'], 'There is 1 item indexed on the server for this index. (<a href=":url">More information</a>)', 'There are @count items indexed on the server for this index. (<a href=":url">More information</a>)', $vars);
}
else {
$args = ['@message' => $variables['server_count_error']];
$info = t('Error while checking server index status: @message', $args);
$classes[] = 'error';
}
$classes[] = 'search-api-index-summary--server-index-status';
$rows[] = Utility::deepCopy($row);
$classes = [];
$cron_limit = $index->getOption('cron_limit', \Drupal::config('search_api.settings')->get('default_cron_limit'));
// Check if the cron limit is higher than zero.
if ($cron_limit != 0) {
$classes[] = 'ok';
if ($cron_limit > 0) {
$info = \Drupal::translation()->formatPlural($cron_limit, 'During cron runs, 1 item will be indexed per batch.', 'During cron runs, @count items will be indexed per batch.');
}
else {
$info = t('All items will be indexed at once during cron runs.');
}
}
else {
$classes[] = 'warning';
$info = t('No items will be indexed during cron runs.');
}
// Append the row and reset variables.
$label = t('Cron batch size');
$classes[] = 'search-api-index-summary--cron-batch-size';
$rows[] = Utility::deepCopy($row);
$classes = [];
// Add the indexing progress bar.
if ($tracker) {
$indexed_count = $tracker->getIndexedItemsCount();
$total_count = $tracker->getTotalItemsCount();
$index_progress = [
'#theme' => 'progress_bar',
'#percent' => $total_count ? (int) (100 * $indexed_count / $total_count) : 100,
'#message' => t('@indexed/@total indexed', ['@indexed' => $indexed_count, '@total' => $total_count]),
];
$output .= '<h3>' . t('Index status') . '</h3>';
$output .= '<div class="search-api-index-status">' . render($index_progress) . '</div>';
}
}
// Append the index info table to the output.
$index_info_table = [
'#theme' => 'table',
'#rows' => $rows,
'#attributes' => [
'class' => [
'search-api-index-summary',
],
],
];
$output .= render($index_info_table);
return $output;
}
@@ -0,0 +1,828 @@
<?php
/**
* @file
* Views hook implementations for the Search API module.
*/
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Item\FieldInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\Utility\Utility;
/**
* Implements hook_views_data().
*
* For each search index, we provide the following tables:
* - One base table, with key "search_api_index_INDEX", which contains field,
* filter, argument and sort handlers for all indexed fields. (Field handlers,
* too, to allow things like click-sorting.)
* - Tables for each datasource, by default with key
* "search_api_datasource_INDEX_DATASOURCE", with field and (where applicable)
* relationship handlers for each property of the datasource. Those will be
* joined to the index base table by default.
*
* Also, for each entity type encountered in any table, a table with
* field/relationship handlers for all of that entity type's properties is
* created. Those tables will use the key "search_api_entity_ENTITY".
*/
function search_api_views_data() {
$data = [];
/** @var \Drupal\search_api\IndexInterface $index */
foreach (Index::loadMultiple() as $index) {
try {
// Fill in base data.
$key = 'search_api_index_' . $index->id();
$table = &$data[$key];
$index_label = $index->label();
$table['table']['group'] = t('Index @name', ['@name' => $index_label]);
$table['table']['base'] = [
'field' => 'search_api_id',
'index' => $index->id(),
'title' => t('Index @name', ['@name' => $index_label]),
'help' => t('Use the @name search index for filtering and retrieving data.', ['@name' => $index_label]),
'query_id' => 'search_api_query',
];
// Add suitable handlers for all indexed fields.
foreach ($index->getFields(TRUE) as $field_id => $field) {
$field_alias = _search_api_views_find_field_alias($field_id, $table);
$field_definition = _search_api_views_get_handlers($field);
// The field handler has to be extra, since it is a) determined by the
// field's underlying property and b) needs a different "real field"
// set.
if ($field->getPropertyPath()) {
$field_handler = _search_api_views_get_field_handler_for_property($field->getDataDefinition(), $field->getPropertyPath());
if ($field_handler) {
$field_definition['field'] = $field_handler;
$field_definition['field']['real field'] = $field->getCombinedPropertyPath();
$field_definition['field']['search_api field'] = $field_id;
}
}
if ($field_definition) {
$field_label = $field->getLabel();
$field_definition += [
'title' => $field_label,
'help' => $field->getDescription() ?: t('(No description available)'),
];
if ($datasource = $field->getDatasource()) {
$field_definition['group'] = t('@datasource datasource', ['@datasource' => $datasource->label()]);
}
if ($field_id != $field_alias) {
$field_definition['real field'] = $field_id;
}
if (isset($field_definition['field'])) {
$field_definition['field']['title'] = t('@field (indexed field)', ['@field' => $field_label]);
}
$table[$field_alias] = $field_definition;
}
}
// Add special fields.
_search_api_views_data_special_fields($table);
// Add relationships for field data of all datasources.
$datasource_tables_prefix = 'search_api_datasource_' . $index->id() . '_';
foreach ($index->getDatasources() as $datasource_id => $datasource) {
$table_key = _search_api_views_find_field_alias($datasource_tables_prefix . $datasource_id, $data);
$data[$table_key] = _search_api_views_datasource_table($datasource, $data);
// Automatically join this table for views of this index.
$data[$table_key]['table']['join'][$key] = [
'join_id' => 'search_api',
];
}
}
catch (\Exception $e) {
$args = [
'%index' => $index->label(),
];
watchdog_exception('search_api', $e, '%type while computing Views data for index %index: @message in %function (line %line of %file).', $args);
}
}
return array_filter($data);
}
/**
* Implements hook_views_plugins_cache_alter().
*/
function search_api_views_plugins_cache_alter(array &$plugins) {
// Collect all base tables provided by this module.
$bases = [];
/** @var \Drupal\search_api\IndexInterface $index */
foreach (Index::loadMultiple() as $index) {
$bases[] = 'search_api_index_' . $index->id();
}
$plugins['search_api']['base'] = $bases;
}
/**
* Implements hook_views_plugins_row_alter().
*/
function search_api_views_plugins_row_alter(array &$plugins) {
// Collect all base tables provided by this module.
$bases = [];
/** @var \Drupal\search_api\IndexInterface $index */
foreach (Index::loadMultiple() as $index) {
$bases[] = 'search_api_index_' . $index->id();
}
$plugins['search_api']['base'] = $bases;
}
/**
* Finds an unused field alias for a field in a Views table definition.
*
* @param string $field_id
* The original ID of the Search API field.
* @param array $table
* The Views table definition.
*
* @return string
* The field alias to use.
*/
function _search_api_views_find_field_alias($field_id, array &$table) {
$base = $field_alias = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field_id);
$i = 0;
while (isset($table[$field_alias])) {
$field_alias = $base . '_' . ++$i;
}
return $field_alias;
}
/**
* Returns the Views handlers to use for a given field.
*
* @param \Drupal\search_api\Item\FieldInterface $field
* The field to add to the definition.
*
* @return array
* The Views definition to add for the given field.
*/
function _search_api_views_get_handlers(FieldInterface $field) {
$mapping = _search_api_views_handler_mapping();
try {
$types = [];
$definition = $field->getDataDefinition();
if ($definition->getSetting('target_type')) {
$types[] = 'entity:' . $definition->getSetting('target_type');
$types[] = 'entity';
}
if ($definition->getSetting('allowed_values')) {
$types[] = 'options';
}
$types[] = $field->getType();
/** @var \Drupal\search_api\DataType\DataTypeInterface $data_type */
$data_type = \Drupal::service('plugin.manager.search_api.data_type')->createInstance($field->getType());
if (!$data_type->isDefault()) {
$types[] = $data_type->getFallbackType();
}
foreach ($types as $type) {
if (isset($mapping[$type])) {
_search_api_views_handler_adjustments($type, $field, $mapping[$type]);
return $mapping[$type];
}
}
}
catch (SearchApiException $e) {
$vars['%index'] = $field->getIndex()->label();
$vars['%field'] = $field->getPrefixedLabel();
watchdog_exception('search_api', $e, '%type while adding Views handlers for field %field on index %index: @message in %function (line %line of %file).', $vars);
}
return [];
}
/**
* Determines the mapping of Search API data types to their Views handlers.
*
* @return array
* An associative array with data types as the keys and Views field data
* definitions as the values. In addition to all normally defined data types,
* keys can also be "options" for any field with an options list, "entity" for
* general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE"
* being the machine name of an entity type) for entities of that type.
*
* @see search_api_views_handler_mapping_alter()
*/
function _search_api_views_handler_mapping() {
$mapping = &drupal_static(__FUNCTION__);
if (!isset($mapping)) {
$mapping = [
'boolean' => [
'argument' => [
'id' => 'search_api',
],
'filter' => [
'id' => 'search_api_boolean',
],
'sort' => [
'id' => 'search_api',
],
],
'date' => [
'argument' => [
'id' => 'search_api_date',
],
'filter' => [
'id' => 'search_api_date',
],
'sort' => [
'id' => 'search_api',
],
],
'decimal' => [
'argument' => [
'id' => 'search_api',
'filter' => 'floatval',
],
'filter' => [
'id' => 'search_api_numeric',
],
'sort' => [
'id' => 'search_api',
],
],
'integer' => [
'argument' => [
'id' => 'search_api',
'filter' => 'intval',
],
'filter' => [
'id' => 'search_api_numeric',
],
'sort' => [
'id' => 'search_api',
],
],
'string' => [
'argument' => [
'id' => 'search_api',
],
'filter' => [
'id' => 'search_api_string',
],
'sort' => [
'id' => 'search_api',
],
],
'text' => [
'argument' => [
'id' => 'search_api',
],
'filter' => [
'id' => 'search_api_text',
],
'sort' => [
'id' => 'search_api',
],
],
'options' => [
'argument' => [
'id' => 'search_api',
],
'filter' => [
'id' => 'search_api_options',
],
'sort' => [
'id' => 'search_api',
],
],
'entity:taxonomy_term' => [
'argument' => [
'id' => 'search_api_term',
],
'filter' => [
'id' => 'search_api_term',
],
'sort' => [
'id' => 'search_api',
],
],
'entity:user' => [
'argument' => [
'id' => 'search_api',
'filter' => 'intval',
],
'filter' => [
'id' => 'search_api_user',
],
'sort' => [
'id' => 'search_api',
],
],
'entity:node_type' => [
'argument' => [
'id' => 'search_api',
],
'filter' => [
'id' => 'search_api_options',
'options callback' => 'node_type_get_names',
],
'sort' => [
'id' => 'search_api',
],
],
];
$alter_id = 'search_api_views_handler_mapping';
\Drupal::moduleHandler()->alter($alter_id, $mapping);
}
return $mapping;
}
/**
* Makes necessary, field-specific adjustments to Views handler definitions.
*
* @param string $type
* The type of field, as defined in _search_api_views_handler_mapping().
* @param \Drupal\search_api\Item\FieldInterface $field
* The field whose handler definitions are being created.
* @param array $definitions
* The handler definitions for the field, as a reference.
*/
function _search_api_views_handler_adjustments($type, FieldInterface $field, array &$definitions) {
// By default, all fields can be empty (or at least have to be treated that
// way by the Search API).
if (!isset($definitions['filter']['allow empty'])) {
$definitions['filter']['allow empty'] = TRUE;
}
// For taxonomy term references, set the referenced vocabulary.
$data_definition = $field->getDataDefinition();
if ($type == 'entity:taxonomy_term') {
if (isset($data_definition->getSettings()['handler_settings']['target_bundles'])) {
$target_bundles = $data_definition->getSettings()['handler_settings']['target_bundles'];
if (count($target_bundles) == 1) {
$definitions['filter']['vocabulary'] = reset($target_bundles);
}
}
}
elseif ($type == 'options') {
if ($data_definition instanceof FieldItemDataDefinition) {
// If this is a normal Field API field, dynamically retrieve the options
// list at query time.
$field_definition = $data_definition->getFieldDefinition();
$bundle = $field_definition->getTargetBundle();
$field_name = $field_definition->getName();
$entity_type = $field_definition->getTargetEntityTypeId();
$definitions['filter']['options callback'] = '_search_api_views_get_allowed_values';
$definitions['filter']['options arguments'] = [$entity_type, $bundle, $field_name];
}
else {
// Otherwise, include the options list verbatim in the Views data, unless
// it's too big (or doesn't look valid).
$options = $data_definition->getSetting('allowed_values');
if (is_array($options) && count($options) <= 50) {
// Since the Views InOperator filter plugin doesn't allow just including
// the options in the definition, we use this workaround.
$definitions['filter']['options callback'] = 'array_filter';
$definitions['filter']['options arguments'] = [$options];
}
}
}
}
/**
* Adds definitions for our special fields to a Views data table definition.
*
* @param array $table
* The existing Views data table definition.
*/
function _search_api_views_data_special_fields(array &$table) {
$id_field = _search_api_views_find_field_alias('search_api_id', $table);
$table[$id_field]['title'] = t('Item ID');
$table[$id_field]['help'] = t("The item's internal (Search API-specific) ID");
$table[$id_field]['field']['id'] = 'standard';
$table[$id_field]['sort']['id'] = 'search_api';
if ($id_field != 'search_api_id') {
$table[$id_field]['real field'] = 'search_api_id';
}
$datasource_field = _search_api_views_find_field_alias('search_api_datasource', $table);
$table[$datasource_field]['title'] = t('Datasource');
$table[$datasource_field]['help'] = t("The data source ID");
$table[$datasource_field]['argument']['id'] = 'search_api';
$table[$datasource_field]['argument']['disable_break_phrase'] = TRUE;
$table[$datasource_field]['field']['id'] = 'standard';
$table[$datasource_field]['filter']['id'] = 'search_api_datasource';
$table[$datasource_field]['sort']['id'] = 'search_api';
if ($datasource_field != 'search_api_datasource') {
$table[$datasource_field]['real field'] = 'search_api_datasource';
}
$language_field = _search_api_views_find_field_alias('search_api_language', $table);
$table[$language_field]['title'] = t('Item language');
$table[$language_field]['help'] = t("The item's language");
$table[$language_field]['field']['id'] = 'language';
$table[$language_field]['filter']['id'] = 'search_api_language';
$table[$language_field]['filter']['allow empty'] = FALSE;
$table[$language_field]['sort']['id'] = 'search_api';
if ($language_field != 'search_api_language') {
$table[$language_field]['real field'] = 'search_api_language';
}
$relevance_field = _search_api_views_find_field_alias('search_api_relevance', $table);
$table[$relevance_field]['group'] = t('Search');
$table[$relevance_field]['title'] = t('Relevance');
$table[$relevance_field]['help'] = t('The relevance of this search result with respect to the query');
$table[$relevance_field]['field']['type'] = 'decimal';
$table[$relevance_field]['field']['id'] = 'numeric';
$table[$relevance_field]['field']['search_api field'] = 'search_api_relevance';
$table[$relevance_field]['sort']['id'] = 'search_api';
if ($relevance_field != 'search_api_relevance') {
$table[$relevance_field]['real field'] = 'search_api_relevance';
}
$excerpt_field = _search_api_views_find_field_alias('search_api_excerpt', $table);
$table[$excerpt_field]['group'] = t('Search');
$table[$excerpt_field]['title'] = t('Excerpt');
$table[$excerpt_field]['help'] = t('The search result excerpted to show found search terms');
$table[$excerpt_field]['field']['id'] = 'search_api';
$table[$excerpt_field]['field']['filter_type'] = 'xss';
if ($excerpt_field != 'search_api_excerpt') {
$table[$excerpt_field]['real field'] = 'search_api_excerpt';
}
$fulltext_field = _search_api_views_find_field_alias('search_api_fulltext', $table);
$table[$fulltext_field]['group'] = t('Search');
$table[$fulltext_field]['title'] = t('Fulltext search');
$table[$fulltext_field]['help'] = t('Search several or all fulltext fields at once.');
$table[$fulltext_field]['filter']['id'] = 'search_api_fulltext';
$table[$fulltext_field]['argument']['id'] = 'search_api_fulltext';
if ($fulltext_field != 'search_api_fulltext') {
$table[$fulltext_field]['real field'] = 'search_api_fulltext';
}
$mlt_field = _search_api_views_find_field_alias('search_api_more_like_this', $table);
$table[$mlt_field]['group'] = t('Search');
$table[$mlt_field]['title'] = t('More like this');
$table[$mlt_field]['help'] = t('Find similar content.');
$table[$mlt_field]['argument']['id'] = 'search_api_more_like_this';
if ($mlt_field != 'search_api_more_like_this') {
$table[$mlt_field]['real field'] = 'search_api_more_like_this';
}
// @todo Add an "All taxonomy terms" contextual filter (if applicable).
}
/**
* Creates a Views table definition for one datasource of an index.
*
* @param \Drupal\search_api\Datasource\DatasourceInterface $datasource
* The datasource for which to create a table definition.
* @param array $data
* The existing Views data definitions. Passed by reference so additionally
* needed tables can be inserted.
*
* @return array
* A Views table definition for the given datasource.
*/
function _search_api_views_datasource_table(DatasourceInterface $datasource, array &$data) {
$datasource_id = $datasource->getPluginId();
$table = [
'table' => [
'group' => t('@datasource datasource', ['@datasource' => $datasource->label()]),
'index' => $datasource->getIndex()->id(),
'datasource' => $datasource_id,
],
];
$entity_type_id = $datasource->getEntityTypeId();
if ($entity_type_id) {
$table['table']['entity type'] = $entity_type_id;
$table['table']['entity revision'] = FALSE;
}
_search_api_views_add_handlers_for_properties($datasource->getPropertyDefinitions(), $table, $data);
// Prefix the "real field" of each entry with the datasource ID.
foreach ($table as $key => $definition) {
if ($key == 'table') {
continue;
}
$real_field = isset($definition['real field']) ? $definition['real field'] : $key;
$table[$key]['real field'] = Utility::createCombinedId($datasource_id, $real_field);
// Relationships sometimes have different real fields set, since they might
// also include the nested property that contains the actual reference. So,
// if a "real field" is set for that, we need to adapt it as well.
if (isset($definition['relationship']['real field'])) {
$real_field = $definition['relationship']['real field'];
$table[$key]['relationship']['real field'] = Utility::createCombinedId($datasource_id, $real_field);
}
}
return $table;
}
/**
* Creates a Views table definition for an entity type.
*
* @param string $entity_type_id
* The ID of the entity type.
* @param array $data
* The existing Views data definitions, passed by reference.
*
* @return array
* A Views table definition for the given entity type. Or an empty array if
* the entity type could not be found.
*/
function _search_api_views_entity_type_table($entity_type_id, array &$data) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
if (!$entity_type || !$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
return [];
}
$table = [
'table' => [
'group' => t('@entity_type relationship', ['@entity_type' => $entity_type->getLabel()]),
'entity type' => $entity_type_id,
'entity revision' => FALSE,
],
];
$entity_field_manager = \Drupal::getContainer()->get('entity_field.manager');
$bundle_info = \Drupal::getContainer()->get('entity_type.bundle.info');
$properties = $entity_field_manager->getBaseFieldDefinitions($entity_type_id);
foreach (array_keys($bundle_info->getBundleInfo($entity_type_id)) as $bundle_id) {
$additional = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle_id);
$properties += $additional;
}
_search_api_views_add_handlers_for_properties($properties, $table, $data);
return $table;
}
/**
* Adds field and relationship handlers for the given properties.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
* The properties for which handlers should be added.
* @param array $table
* The existing Views data table definition, passed by reference.
* @param array $data
* The existing Views data definitions, passed by reference.
*/
function _search_api_views_add_handlers_for_properties(array $properties, array &$table, array &$data) {
$entity_reference_types = array_flip([
'field_item:entity_reference',
'field_item:image',
'field_item:file',
]);
foreach ($properties as $property_path => $property) {
$key = _search_api_views_find_field_alias($property_path, $table);
$original_property = $property;
$property = \Drupal::getContainer()
->get('search_api.fields_helper')
->getInnerProperty($property);
// Add a field handler, if applicable.
$definition = _search_api_views_get_field_handler_for_property($property, $property_path);
if ($definition) {
$table[$key]['field'] = $definition;
}
// For entity-typed properties, also add a relationship to the entity type
// table.
if ($property instanceof FieldItemDataDefinition && isset($entity_reference_types[$property->getDataType()])) {
$entity_type_id = $property->getSetting('target_type');
if ($entity_type_id) {
$entity_type_table_key = 'search_api_entity_' . $entity_type_id;
if (!isset($data[$entity_type_table_key])) {
// Initialize the table definition before calling
// _search_api_views_entity_type_table() to avoid an infinite
// recursion.
$data[$entity_type_table_key] = [];
$data[$entity_type_table_key] = _search_api_views_entity_type_table($entity_type_id, $data);
}
// Add the relationship only if we have a non-empty table definition.
if ($data[$entity_type_table_key]) {
// Get the entity type to determine the label for the relationship.
$entity_type = \Drupal::entityTypeManager()
->getDefinition($entity_type_id);
$entity_type_label = $entity_type ? $entity_type->getLabel() : $entity_type_id;
$args = [
'@label' => $entity_type_label,
'@field_name' => $original_property->getLabel(),
];
// Look through the child properties to find the data reference
// property that should be the "real field" for the relationship.
// (For Core entity references, this will usually be ":entity".)
$suffix = '';
foreach ($property->getPropertyDefinitions() as $name => $nested_property) {
if ($nested_property instanceof DataReferenceDefinitionInterface) {
$suffix = ":$name";
break;
}
}
$table[$key]['relationship'] = [
'title' => t('@label referenced from @field_name', $args),
'label' => t('@field_name: @label', $args),
'help' => $property->getDescription() ?: t('(No description available)'),
'id' => 'search_api',
'base' => $entity_type_table_key,
'entity type' => $entity_type_id,
'entity revision' => FALSE,
'real field' => $property_path . $suffix,
];
}
}
}
if (!empty($table[$key]) && empty($table[$key]['title'])) {
$table[$key]['title'] = $original_property->getLabel();
$table[$key]['help'] = $original_property->getDescription() ?: t('(No description available)');
if ($key != $property_path) {
$table[$key]['real field'] = $property_path;
}
}
}
}
/**
* Computes a handler definition for the given property.
*
* @param \Drupal\Core\TypedData\DataDefinitionInterface $property
* The property definition.
* @param string|null $property_path
* (optional) The property path of the property. If set, it will be used for
* Field API fields to set the "field_name" property of the definition.
*
* @return array|null
* Either a Views field handler definition for this property, or NULL if the
* property shouldn't have one.
*
* @see hook_search_api_views_field_handler_mapping_alter()
*/
function _search_api_views_get_field_handler_for_property(DataDefinitionInterface $property, $property_path = NULL) {
$mappings = _search_api_views_get_field_handler_mapping();
// First, look for an exact match.
$data_type = $property->getDataType();
if (array_key_exists($data_type, $mappings['simple'])) {
$definition = $mappings['simple'][$data_type];
}
else {
// Then check all the patterns defined by regular expressions, defaulting to
// the "default" definition.
$definition = $mappings['default'];
foreach (array_keys($mappings['regex']) as $regex) {
if (preg_match($regex, $data_type)) {
$definition = $mappings['regex'][$regex];
}
}
}
// Field items have a special handler, but need a fallback handler set to be
// able to optionally circumvent entity field rendering. That's why we just
// set the "field_item:…" types to their fallback handlers in
// _search_api_views_get_field_handler_mapping(), along with non-field item
// types, and here manually update entity field properties to have the correct
// definition, with "search_api_field" handler, correct fallback handler and
// "field_name" and "entity_type" correctly set.
if (isset($definition) && $property instanceof FieldItemDataDefinition) {
list(, $field_name) = Utility::splitPropertyPath($property_path, TRUE);
if (!isset($definition['fallback_handler'])) {
$definition['fallback_handler'] = $definition['id'];
$definition['id'] = 'search_api_field';
}
$definition['field_name'] = $field_name;
$definition['entity_type'] = $property
->getFieldDefinition()
->getTargetEntityTypeId();
}
return $definition;
}
/**
* Retrieves the field handler mapping used by the Search API Views integration.
*
* @return array
* An associative array with three keys:
* - simple: An associative array mapping property data types to their field
* handler definitions.
* - regex: An array associative array mapping regular expressions for
* property data types to their field handler definitions, ordered by
* descending string length of the regular expression.
* - default: The default definition for data types that match no other field.
*/
function _search_api_views_get_field_handler_mapping() {
$mappings = &drupal_static(__FUNCTION__);
if (!isset($mappings)) {
// First create a plain mapping and pass it to the alter hook.
$plain_mapping = [];
$plain_mapping['*'] = [
'id' => 'search_api',
];
$text_mapping = [
'id' => 'search_api',
'filter_type' => 'xss',
];
$plain_mapping['field_item:text_long'] = $text_mapping;
$plain_mapping['field_item:text_with_summary'] = $text_mapping;
$plain_mapping['search_api_html'] = $text_mapping;
unset($text_mapping['filter_type']);
$plain_mapping['search_api_text'] = $text_mapping;
$numeric_mapping = [
'id' => 'search_api_numeric',
];
$plain_mapping['field_item:integer'] = $numeric_mapping;
$plain_mapping['field_item:list_integer'] = $numeric_mapping;
$plain_mapping['integer'] = $numeric_mapping;
$plain_mapping['timespan'] = $numeric_mapping;
$float_mapping = [
'id' => 'search_api_numeric',
'float' => TRUE,
];
$plain_mapping['field_item:decimal'] = $float_mapping;
$plain_mapping['field_item:float'] = $float_mapping;
$plain_mapping['field_item:list_float'] = $float_mapping;
$plain_mapping['decimal'] = $float_mapping;
$plain_mapping['float'] = $float_mapping;
$date_mapping = [
'id' => 'search_api_date',
];
$plain_mapping['field_item:created'] = $date_mapping;
$plain_mapping['field_item:changed'] = $date_mapping;
$plain_mapping['datetime_iso8601'] = $date_mapping;
$plain_mapping['timestamp'] = $date_mapping;
$bool_mapping = [
'id' => 'search_api_boolean',
];
$plain_mapping['boolean'] = $bool_mapping;
$plain_mapping['field_item:boolean'] = $bool_mapping;
$ref_mapping = [
'id' => 'search_api_entity',
];
$plain_mapping['field_item:entity_reference'] = $ref_mapping;
$plain_mapping['field_item:comment'] = $ref_mapping;
// Finally, set a default handler for unknown field items.
$plain_mapping['field_item:*'] = [
'id' => 'search_api',
];
// Let other modules change or expand this mapping.
$alter_id = 'search_api_views_field_handler_mapping';
\Drupal::moduleHandler()->alter($alter_id, $plain_mapping);
// Then create a new, more practical structure, with the mappings grouped by
// mapping type.
$mappings = [
'simple' => [],
'regex' => [],
'default' => NULL,
];
foreach ($plain_mapping as $type => $definition) {
if ($type == '*') {
$mappings['default'] = $definition;
}
elseif (strpos($type, '*') === FALSE) {
$mappings['simple'][$type] = $definition;
}
else {
// Transform the type into a PCRE regular expression, taking care to
// quote everything except for the wildcards.
$parts = explode('*', $type);
// Passing the second parameter to preg_quote() is a bit tricky with
// array_map(), we need to construct an array of slashes.
$slashes = array_fill(0, count($parts), '/');
$parts = array_map('preg_quote', $parts, $slashes);
// Use the "S" modifier for closer analysis of the pattern, since it
// might be executed a lot.
$regex = '/^' . implode('.*', $parts) . '$/S';
$mappings['regex'][$regex] = $definition;
}
}
// Finally, order the regular expressions descending by their lengths.
$compare = function ($a, $b) {
return strlen($b) - strlen($a);
};
uksort($mappings['regex'], $compare);
}
return $mappings;
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API backend annotation object.
*
* @see \Drupal\search_api\Backend\BackendPluginManager
* @see \Drupal\search_api\Backend\BackendInterface
* @see \Drupal\search_api\Backend\BackendPluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiBackend extends Plugin {
/**
* The backend plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the backend plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The backend description.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
}
@@ -0,0 +1,60 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API data type annotation object.
*
* @see \Drupal\search_api\DataType\DataTypePluginManager
* @see \Drupal\search_api\DataType\DataTypeInterface
* @see \Drupal\search_api\DataType\DataTypePluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiDataType extends Plugin {
/**
* The data type plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the data type plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The description of the data type.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
/**
* Whether this is one of the default data types provided by the Search API.
*
* @var bool
*/
public $default = FALSE;
/**
* The ID of the fallback data type for this data type.
*
* Needs to be one of the default data types defined in the Search API itself.
*
* @var string
*/
public $fallback_type = 'string';
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API datasource annotation object.
*
* @see \Drupal\search_api\Datasource\DatasourcePluginManager
* @see \Drupal\search_api\Datasource\DatasourceInterface
* @see \Drupal\search_api\Datasource\DatasourcePluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiDatasource extends Plugin {
/**
* The datasource plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the datasource plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* The description of the datasource.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API display annotation object.
*
* @see \Drupal\search_api\Display\DisplayPluginManager
* @see \Drupal\search_api\Display\DisplayInterface
* @see \Drupal\search_api\Display\DisplayPluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiDisplay extends Plugin {
/**
* The display plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the display plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The human-readable description for the display plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
/**
* The ID of the display's index.
*
* @var string
*/
public $index;
/**
* The path to the search display, if any.
*
* @var string|null
*/
public $path;
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API parse mode annotation object.
*
* @see \Drupal\search_api\ParseMode\ParseModePluginManager
* @see \Drupal\search_api\ParseMode\ParseModeInterface
* @see \Drupal\search_api\ParseMode\ParseModePluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiParseMode extends Plugin {
/**
* The parse mode plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the parse mode.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The description of the parse mode.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
}
@@ -0,0 +1,56 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API processor annotation object.
*
* @see \Drupal\search_api\Processor\ProcessorPluginManager
* @see \Drupal\search_api\Processor\ProcessorInterface
* @see \Drupal\search_api\Processor\ProcessorPluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiProcessor extends Plugin {
/**
* The processor plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the processor plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The description of the processor.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
/**
* The stages this processor will run in, along with their default weights.
*
* This is represented as an associative array, mapping one or more of the
* stage identifiers to the default weight for that stage. For the available
* stages, see
* \Drupal\search_api\Processor\ProcessorPluginManager::getProcessingStages().
*
* @var int[]
*/
public $stages;
}
@@ -0,0 +1,44 @@
<?php
namespace Drupal\search_api\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Search API tracker annotation object.
*
* @see \Drupal\search_api\Tracker\TrackerPluginManager
* @see \Drupal\search_api\Tracker\TrackerInterface
* @see \Drupal\search_api\Tracker\TrackerPluginBase
* @see plugin_api
*
* @Annotation
*/
class SearchApiTracker extends Plugin {
/**
* The tracker plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the tracker plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* The description of the tracker.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $description;
}
@@ -0,0 +1,86 @@
<?php
namespace Drupal\search_api\Backend;
use Drupal\search_api\Plugin\ConfigurablePluginInterface;
use Drupal\search_api\ServerInterface;
/**
* Defines an interface for search backend plugins.
*
* Consists of general plugin methods and the backend-specific methods defined
* in \Drupal\search_api\Backend\BackendSpecificInterface, as well as special
* CRUD "hook" methods that cannot be present on the server entity (which also
* implements \Drupal\search_api\Backend\BackendSpecificInterface).
*
* @see \Drupal\search_api\Annotation\SearchApiBackend
* @see \Drupal\search_api\Backend\BackendPluginManager
* @see \Drupal\search_api\Backend\BackendPluginBase
* @see plugin_api
*/
interface BackendInterface extends ConfigurablePluginInterface, BackendSpecificInterface {
/**
* Retrieves the server entity for this backend.
*
* @return \Drupal\search_api\ServerInterface
* The server entity.
*/
public function getServer();
/**
* Sets the server entity for this backend.
*
* @param \Drupal\search_api\ServerInterface $server
* The server entity.
*
* @return $this
*/
public function setServer(ServerInterface $server);
/**
* Reacts to the server's creation.
*
* Called once, when the server is first created. Allows the backend class to
* set up its necessary infrastructure.
*/
public function postInsert();
/**
* Notifies the backend that its configuration is about to be updated.
*
* The server's $original property can be used to inspect the old
* configuration values.
*
* Take care, though, that the server at this point might be override-free and
* thus contain property values (and apparent changes) which will not actually
* go into effect. If this might influence the code in this method, you have
* to manually check for overrides to ensure no incorrect action is taken.
*
* @see \Drupal\search_api\Utility\Utility::getConfigOverrides()
*/
public function preUpdate();
/**
* Notifies the backend that its configuration was updated.
*
* The server's $original property can be used to inspect the old
* configuration values.
*
* @return bool
* TRUE, if the update requires reindexing of all content on the server.
*/
public function postUpdate();
/**
* Notifies the backend that the server is about to be deleted.
*
* This should execute any necessary cleanup operations.
*
* Note that you shouldn't call the server's save() method, or any
* methods that might do that, from inside of this method as the server isn't
* present in the database anymore at this point.
*/
public function preDelete();
}
@@ -0,0 +1,327 @@
<?php
namespace Drupal\search_api\Backend;
use Drupal\search_api\Entity\Server;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Plugin\ConfigurablePluginBase;
use Drupal\search_api\ServerInterface;
use Drupal\search_api\Utility\FieldsHelper;
/**
* Defines a base class for backend plugins.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. These definition arrays may be altered through
* hook_search_api_backend_info_alter(). The definition includes the following
* keys:
* - id: The unique, system-wide identifier of the backend class.
* - label: The human-readable name of the backend class, translated.
* - description: A human-readable description for the backend class,
* translated.
*
* A complete plugin definition should be written as in this example:
*
* @code
* @SearchApiBackend(
* id = "my_backend",
* label = @Translation("My backend"),
* description = @Translation("Searches with SuperSearch™.")
* )
* @endcode
*
* @see \Drupal\search_api\Annotation\SearchApiBackend
* @see \Drupal\search_api\Backend\BackendPluginManager
* @see \Drupal\search_api\Backend\BackendInterface
* @see plugin_api
*/
abstract class BackendPluginBase extends ConfigurablePluginBase implements BackendInterface {
use LoggerTrait;
/**
* The server this backend is configured for.
*
* @var \Drupal\search_api\ServerInterface
*/
protected $server;
/**
* The backend's server's ID.
*
* Used for serialization.
*
* @var string
*/
protected $serverId;
/**
* The fields helper.
*
* @var \Drupal\search_api\Utility\FieldsHelper|null
*/
protected $fieldsHelper;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
if (!empty($configuration['#server']) && $configuration['#server'] instanceof ServerInterface) {
$this->setServer($configuration['#server']);
unset($configuration['#server']);
}
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* Retrieves the fields helper.
*
* @return \Drupal\search_api\Utility\FieldsHelper
* The fields helper.
*/
public function getFieldsHelper() {
return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
}
/**
* Sets the fields helper.
*
* @param \Drupal\search_api\Utility\FieldsHelper $fields_helper
* The new fields helper.
*
* @return $this
*/
public function setFieldsHelper(FieldsHelper $fields_helper) {
$this->fieldsHelper = $fields_helper;
return $this;
}
/**
* {@inheritdoc}
*/
public function getServer() {
return $this->server;
}
/**
* {@inheritdoc}
*/
public function setServer(ServerInterface $server) {
$this->server = $server;
return $this;
}
/**
* {@inheritdoc}
*/
public function viewSettings() {
return [];
}
/**
* {@inheritdoc}
*/
public function isAvailable() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getSupportedFeatures() {
return [];
}
/**
* {@inheritdoc}
*/
public function supportsDataType($type) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function postInsert() {}
/**
* {@inheritdoc}
*/
public function preUpdate() {}
/**
* {@inheritdoc}
*/
public function postUpdate() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function preDelete() {
try {
$this->getServer()->deleteAllItems();
}
catch (SearchApiException $e) {
$vars = [
'%server' => $this->getServer()->label(),
];
$this->logException($e, '%type while deleting items from server %server: @message in %function (line %line of %file).', $vars);
drupal_set_message($this->t('Deleting some of the items on the server failed. Check the logs for details. The server was still removed.'), 'error');
}
}
/**
* {@inheritdoc}
*/
public function getBackendDefinedFields(IndexInterface $index) {
return [];
}
/**
* {@inheritdoc}
*/
public function addIndex(IndexInterface $index) {}
/**
* {@inheritdoc}
*/
public function updateIndex(IndexInterface $index) {}
/**
* {@inheritdoc}
*/
public function removeIndex($index) {
// Only delete the index's data if the index isn't read-only. (If only the
// ID is given, we assume the index was read-only, to be on the safe side.)
if ($index instanceof IndexInterface && !$index->isReadOnly()) {
$this->deleteAllIndexItems($index);
}
}
/**
* {@inheritdoc}
*/
public function getDiscouragedProcessors() {
return [];
}
/**
* Creates dummy field objects for the "magic" fields present for every index.
*
* @param \Drupal\search_api\IndexInterface $index
* The index for which to create the fields. (Needed since field objects
* always need an index set.)
* @param \Drupal\search_api\Item\ItemInterface|null $item
* (optional) If given, an item whose data should be used for the fields'
* values.
*
* @return \Drupal\search_api\Item\FieldInterface[]
* An array of field objects for all "magic" fields, keyed by field IDs.
*/
protected function getSpecialFields(IndexInterface $index, ItemInterface $item = NULL) {
$field_info = [
'type' => 'string',
'original type' => 'string',
];
$fields['search_api_id'] = $this->getFieldsHelper()
->createField($index, 'search_api_id', $field_info);
$fields['search_api_datasource'] = $this->getFieldsHelper()
->createField($index, 'search_api_datasource', $field_info);
$fields['search_api_language'] = $this->getFieldsHelper()
->createField($index, 'search_api_language', $field_info);
if ($item) {
$fields['search_api_id']->setValues([$item->getId()]);
$fields['search_api_datasource']->setValues([$item->getDatasourceId()]);
$fields['search_api_language']->setValues([$item->getLanguage()]);
}
return $fields;
}
/**
* Verifies that the given condition operator is valid for this backend.
*
* @param string $operator
* The operator in question.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if the operator is not known.
*
* @see \Drupal\search_api\Query\ConditionSetInterface::addCondition()
*/
protected function validateOperator($operator) {
switch ($operator) {
case '=':
case '<>':
case '<':
case '<=':
case '>=':
case '>':
case 'IN':
case 'NOT IN':
case 'BETWEEN':
case 'NOT BETWEEN':
return;
}
throw new SearchApiException("Unknown operator '$operator' used in search query condition");
}
/**
* Implements the magic __sleep() method.
*
* Prevents the server entity from being serialized.
*/
public function __sleep() {
if ($this->server) {
$this->serverId = $this->server->id();
}
$properties = array_flip(parent::__sleep());
unset($properties['server']);
return array_keys($properties);
}
/**
* Implements the magic __wakeup() method.
*
* Reloads the server entity.
*/
public function __wakeup() {
parent::__wakeup();
if ($this->serverId) {
$this->server = Server::load($this->serverId);
$this->serverId = NULL;
}
}
/**
* 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 \Drupal\search_api\Query\QueryInterface $query
* The search query.
*
* @return string[]
* The fulltext fields in which to search for the search keys.
*
* @see \Drupal\search_api\Query\QueryInterface::getFulltextFields()
*/
protected function getQueryFulltextFields(QueryInterface $query) {
$fulltext_fields = $query->getFulltextFields();
$index_fields = $query->getIndex()->getFulltextFields();
return $fulltext_fields === NULL ? $index_fields : array_intersect($fulltext_fields, $index_fields);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\search_api\Backend;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages search backend plugins.
*
* @see \Drupal\search_api\Annotation\SearchApiBackend
* @see \Drupal\search_api\Backend\BackendInterface
* @see \Drupal\search_api\Backend\BackendPluginBase
* @see plugin_api
*/
class BackendPluginManager extends DefaultPluginManager {
/**
* Constructs a BackendPluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/search_api/backend', $namespaces, $module_handler, 'Drupal\search_api\Backend\BackendInterface', 'Drupal\search_api\Annotation\SearchApiBackend');
$this->setCacheBackend($cache_backend, 'search_api_backends');
$this->alterInfo('search_api_backend_info');
}
}
@@ -0,0 +1,216 @@
<?php
namespace Drupal\search_api\Backend;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Query\QueryInterface;
/**
* Defines methods common to search servers and backend plugins.
*
* This is separate from \Drupal\search_api\Backend\BackendInterface since the
* CRUD reaction methods in the server entity differ from those for the backend
* plugin.
*/
interface BackendSpecificInterface {
/**
* Returns additional, backend-specific information about this server.
*
* This information will be then added to the server's "View" tab in some way.
* In the default theme implementation the data is output in a table with two
* columns along with other, generic information about the server.
*
* @return array
* An array of additional server information, with each piece of information
* being an associative array with the following keys:
* - label: The human-readable label for this data.
* - info: The information, as HTML.
* - status: (optional) The status associated with this information. One of
* "info", "ok", "warning" or "error". Defaults to "info".
*/
public function viewSettings();
/**
* Returns a boolean with the availability of the backend.
*
* This can implement a specific call to test if the backend is available for
* reading. For SOLR or elasticsearch this would be a "ping" to the server to
* test if it's online. If this is a db-backend that is running on a separate
* server, this can also be a ping. When it's a db-backend that runs in the
* same database as the drupal installation; just returning TRUE is enough.
*
* @return bool
* The availability of the backend.
*/
public function isAvailable();
/**
* Returns all features that this backend supports.
*
* Features are optional extensions to Search API functionality and usually
* defined and used by third-party modules.
*
* There are currently two features defined directly in the Search API module:
* - search_api_mlt, by the
* \Drupal\search_api\Plugin\views\argument\SearchApiMoreLikeThis class.
* - search_api_random_sort, by the
* \Drupal\search_api\Plugin\views\query\SearchApiQuery class.
*
* @return string[]
* The identifiers of all features this backend supports.
*
* @see hook_search_api_server_features_alter()
*/
public function getSupportedFeatures();
/**
* Determines whether the backend supports a given add-on data type.
*
* @param string $type
* The identifier of the add-on data type.
*
* @return bool
* TRUE if the backend supports that data type.
*/
public function supportsDataType($type);
/**
* Limits the processors displayed in the UI for indexes on this server.
*
* Returns an array of processor IDs that should not be enabled for this
* backend. It is a bad idea, for example, to have the "Tokenizer" processor
* enabled when using a Solr backend.
*
* @return string[]
* A list of processor IDs.
*/
public function getDiscouragedProcessors();
/**
* Provides information on additional fields made available by the backend.
*
* If a backend indexes additional data with items and wants to make this
* available as fixed fields on the index (for example, to be used with
* Views), it can implement this method to facilitate this.
*
* Fields returned here are expected to work correctly with this server when
* used in query conditions, sorts or similar places.
*
* @param \Drupal\search_api\IndexInterface $index
* The index for which fields are being determined.
*
* @return \Drupal\search_api\Item\FieldInterface[]
* An array of additional fields that are available for this index, keyed by
* their field IDs. The field IDs should always start with "search_api_"
* (avoiding the special field IDs defined by
* \Drupal\search_api\Query\QueryInterface::sort()) to avoid conflicts with
* user-defined fields.
*/
public function getBackendDefinedFields(IndexInterface $index);
/**
* Adds a new index to this server.
*
* If the index was already added to the server, the object should treat this
* as if removeIndex() and then addIndex() were called.
*
* @param \Drupal\search_api\IndexInterface $index
* The index to add.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an error occurred while adding the index.
*/
public function addIndex(IndexInterface $index);
/**
* Notifies the server that an index attached to it has been changed.
*
* If any user action is necessary as a result of this, the method should
* use drupal_set_message() to notify the user.
*
* @param \Drupal\search_api\IndexInterface $index
* The updated index.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an error occurred while reacting to the change.
*/
public function updateIndex(IndexInterface $index);
/**
* Removes an index from this server.
*
* This might mean that the index has been deleted, or reassigned to a
* different server. If you need to distinguish between these cases, inspect
* $index->getServerId().
*
* If the index wasn't added to the server previously, the method call should
* be ignored.
*
* Implementations of this method should also check whether
* $index->isReadOnly() and don't delete any indexed data if it is.
*
* @param \Drupal\search_api\IndexInterface|string $index
* Either an object representing the index to remove, or its ID (if the
* index was completely deleted).
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an error occurred while removing the index.
*/
public function removeIndex($index);
/**
* Indexes the specified items.
*
* @param \Drupal\search_api\IndexInterface $index
* The search index for which items should be indexed.
* @param \Drupal\search_api\Item\ItemInterface[] $items
* An array of items to be indexed, keyed by their item IDs.
*
* @return string[]
* The IDs of all items that were successfully indexed.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if indexing was prevented by a fundamental configuration error.
*/
public function indexItems(IndexInterface $index, array $items);
/**
* Deletes the specified items from the index.
*
* @param \Drupal\search_api\IndexInterface $index
* The index from which items should be deleted.
* @param string[] $item_ids
* The IDs of the deleted items.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an error occurred while trying to delete the items.
*/
public function deleteItems(IndexInterface $index, array $item_ids);
/**
* Deletes all the items from the index.
*
* @param \Drupal\search_api\IndexInterface $index
* The index for which items should be deleted.
* @param string|null $datasource_id
* (optional) If given, only delete items from the datasource with the
* given ID.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an error occurred while trying to delete indexed items.
*/
public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL);
/**
* Executes a search on this server.
*
* @param \Drupal\search_api\Query\QueryInterface $query
* The query to execute.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an error prevented the search from completing.
*/
public function search(QueryInterface $query);
}
@@ -0,0 +1,458 @@
<?php
namespace Drupal\search_api\Commands;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\search_api\Contrib\RowsOfMultiValueFields;
use Drupal\search_api\Utility\CommandHelper;
use Drush\Commands\DrushCommands;
use Psr\Log\LoggerInterface;
/**
* Defines Drush commands for the Search API.
*/
class SearchApiCommands extends DrushCommands {
/**
* The command helper.
*
* @var \Drupal\search_api\Utility\CommandHelper
*/
protected $commandHelper;
/**
* Constructs a SearchApiCommands object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager, ModuleHandlerInterface $moduleHandler) {
$this->commandHelper = new CommandHelper($entityTypeManager, $moduleHandler, 'dt');
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger) {
parent::setLogger($logger);
$this->commandHelper->setLogger($logger);
}
/**
* Lists all search indexes.
*
* @command search-api:list
*
* @usage drush search-api:list
* List all search indexes.
*
* @field-labels
* id: ID
* name: Name
* server: Server ID
* serverName: Server name
* types: Type IDs
* typeNames: Type names
* status: Status
* limit: Limit
*
* @default-fields id,name,serverName,typeNames,status,limit
*
* @aliases sapi-l,search-api-list
*
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
* The table rows.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if an index has a server which couldn't be loaded.
*/
public function listCommand() {
$rows = $this->commandHelper->indexListCommand();
return new RowsOfMultiValueFields($rows);
}
/**
* Enables one disabled search index.
*
* @param string $indexId
* A search index ID.
*
* @command search-api:enable
*
* @usage drush search-api:enable node_index
* Enable the search index with the ID node_index.
*
* @aliases sapi-en,search-api-enable
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if no indexes could be loaded.
*/
public function enable($indexId) {
$this->commandHelper->enableIndexCommand([$indexId]);
}
/**
* Enables all disabled search indexes.
*
* @command search-api:enable-all
*
* @usage drush search-api:enable-all
* Enable all disabled indexes.
* @usage drush sapi-ena
* Alias to enable all disabled indexes.
*
* @aliases sapi-ena,search-api-enable-all
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if no indexes could be loaded.
*/
public function enableAll() {
$this->commandHelper->enableIndexCommand();
}
/**
* Disables one or more enabled search indexes.
*
* @param string $indexId
* A search index ID.
*
* @command search-api:disable
*
* @usage drush search-api:disable node_index
* Disable the search index with the ID node_index.
* @usage drush sapi-dis node_index
* Alias to disable the search index with the ID node_index.
*
* @aliases sapi-dis,search-api-disable
*
* @throws \Exception
* If no indexes are defined or no index has been passed.
*/
public function disable($indexId) {
$this->commandHelper->disableIndexCommand([$indexId]);
}
/**
* Disables all enabled search indexes.
*
* @command search-api:disable-all
*
* @usage drush search-api:disable-all
* Disable all enabled indexes.
* @usage drush sapi-disa
* Alias to disable all enabled indexes.
*
* @aliases sapi-disa,search-api-disable-all
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if no indexes could be loaded.
*/
public function disableAll() {
$this->commandHelper->disableIndexCommand();
}
/**
* Shows the status of one or all search indexes.
*
* @param string|null $indexId
* (optional) A search index ID, or NULL to show the status of all indexes.
*
* @command search-api:status
*
* @usage drush search-api:status
* Show the status of all search indexes.
* @usage drush sapi-s
* Alias to show the status of all search indexes.
* @usage drush sapi-s node_index
* Show the status of the search index with the ID node_index.
*
* @field-labels
* id: ID
* name: Name
* complete: % Complete
* indexed: Indexed
* total: Total
*
* @aliases sapi-s,search-api-status
*
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
* The table rows.
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if one of the affected indexes had an invalid tracker set.
*/
public function status($indexId = NULL) {
$rows = $this->commandHelper->indexStatusCommand([$indexId]);
return new RowsOfFields($rows);
}
/**
* Indexes items for one or all enabled search indexes.
*
* @param string $indexId
* (optional) A search index ID, or NULL to index items for all enabled
* indexes.
* @param array $options
* (optional) An array of options.
*
* @command search-api:index
*
* @option limit
* The maximum number of items to index. Set to 0 to index all items.
* Defaults to 0 (index all).
* @option batch-size
* The maximum number of items to index per batch run. Set to 0 to index all
* items at once. Defaults to the "Cron batch size" setting of the index.
*
* @usage drush search-api:index
* Index all items for all enabled indexes.
* @usage drush sapi-i
* Alias to index all items for all enabled indexes.
* @usage drush sapi-i node_index
* Index all items for the index with the ID node_index.
* @usage drush sapi-i node_index 100
* Index a maximum number of 100 items for the index with the ID node_index.
* @usage drush sapi-i node_index 100 10
* Index a maximum number of 100 items (10 items per batch run) for the
* index with the ID node_index.
*
* @aliases sapi-i,search-api-index
*
* @throws \Exception
* If a batch process could not be created.
*/
public function index($indexId = NULL, array $options = ['limit' => NULL, 'batch-size' => NULL]) {
$limit = $options['limit'];
$batch_size = $options['batch-size'];
$process_batch = $this->commandHelper->indexItemsToIndexCommand([$indexId], $limit, $batch_size);
if ($process_batch === TRUE) {
drush_backend_batch_process();
}
}
/**
* Marks one or all indexes for reindexing without deleting existing data.
*
* @param string $indexId
* The machine name of an index. Optional. If missed, will schedule all
* search indexes for reindexing.
* @param array $options
* An array of options.
*
* @command search-api:reset-tracker
*
* @option entity-types List of entity type ids to reset tracker for.
*
* @usage drush search-api:reset-tracker
* Schedule all search indexes for reindexing.
* @usage drush sapi-r
* Alias to schedule all search indexes for reindexing .
* @usage drush sapi-r node_index
* Schedule the search index with the ID node_index for reindexing.
*
* @aliases search-api-mark-all,search-api-reindex,sapi-r,search-api-reset-tracker
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if one of the affected indexes had an invalid tracker set, or some
* other internal error occurred.
*/
public function resetTracker($indexId = NULL, array $options = ['entity-types' => []]) {
$this->commandHelper->resetTrackerCommand([$indexId], $options['entity-types']);
}
/**
* Clears one or all search indexes and marks them for reindexing.
*
* @param string $indexId
* The machine name of an index. Optional. If missed all search indexes will
* be cleared.
*
* @command search-api:clear
*
* @usage drush search-api:clear
* Clear all search indexes.
* @usage drush sapi-c
* Alias to clear all search indexes.
* @usage drush sapi-c node_index
* Clear the search index with the ID node_index.
*
* @aliases sapi-c,search-api-clear
*
* @throws \Drupal\search_api\SearchApiException
* Thrown if one of the affected indexes had an invalid tracker set, or some
* other internal error occurred.
*/
public function clear($indexId = NULL) {
$this->commandHelper->clearIndexCommand([$indexId]);
}
/**
* Searches for a keyword or phrase in a given index.
*
* @param string $indexId
* The machine name of an index.
* @param string $keyword
* The keyword to look for.
*
* @command search-api:search
*
* @usage drush search-api:search node_index title
* Search for "title" inside the "node_index" index.
* @usage drush sapi-search node_index title
* Alias to search for "title" inside the "node_index" index.
*
* @field-labels
* id: ID
* label: Label
*
* @aliases sapi-search,search-api-search
*
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
* The table rows.
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if searching failed for any reason.
* @throws \Drupal\search_api\SearchApiException
* Thrown if no search query could be created for the given index, for
* example because it is disabled or its server could not be loaded.
*/
public function search($indexId, $keyword) {
$rows = $this->commandHelper->searchIndexCommand($indexId, $keyword);
return new RowsOfFields($rows);
}
/**
* Lists all search servers.
*
* @command search-api:server-list
*
* @usage drush search-api:server-list
* List all search servers.
* @usage drush sapi-sl
* Alias to list all search servers.
*
* @field-labels
* id: ID
* name: Name
* status: Status
*
* @aliases sapi-sl,search-api-server-list
*
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
* The table rows.
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if no servers could be loaded.
*/
public function serverList() {
$rows = $this->commandHelper->serverListCommand();
return new RowsOfFields($rows);
}
/**
* Enables a search server.
*
* @param string $serverId
* The machine name of a server.
*
* @command search-api:server-enable
*
* @usage drush search-api:server-enable my_solr_server
* Enable the my_solr_server search server.
* @usage drush sapi-se my_solr_server
* Alias to enable the my_solr_server search server.
*
* @aliases sapi-se,search-api-server-enable
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if the server couldn't be loaded.
* @throws \Drupal\Core\Entity\EntityStorageException
* Thrown if an internal error occurred when saving the server.
*/
public function serverEnable($serverId) {
$this->commandHelper->enableServerCommand($serverId);
}
/**
* Disables a search server.
*
* @param string $serverId
* The machine name of a server.
*
* @command search-api:server-disable
*
* @usage drush search-api:server-disable
* Disable the my_solr_server search server.
* @usage drush sapi-sd
* Alias to disable the my_solr_server search server.
*
* @aliases sapi-sd,search-api-server-disable
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if the server couldn't be loaded.
* @throws \Drupal\Core\Entity\EntityStorageException
* Thrown if an internal error occurred when saving the server.
*/
public function serverDisable($serverId) {
$this->commandHelper->disableServerCommand($serverId);
}
/**
* Clears all search indexes on the given search server.
*
* @param string $serverId
* The machine name of a server.
*
* @command search-api:server-clear
*
* @usage drush search-api:server-clear my_solr_server
* Clear all search indexes on the search server my_solr_server.
* @usage drush sapi-sc my_solr_server
* Alias to clear all search indexes on the search server my_solr_server.
*
* @aliases sapi-sc,search-api-server-clear
*
* @throws \Drupal\search_api\ConsoleException
* Thrown if the server couldn't be loaded.
* @throws \Drupal\search_api\SearchApiException
* Thrown if one of the affected indexes had an invalid tracker set, or some
* other internal error occurred.
*/
public function serverClear($serverId) {
$this->commandHelper->clearServerCommand($serverId);
}
/**
* Sets the search server used by a given index.
*
* @param string $indexId
* The machine name of an index.
* @param string $serverId
* The machine name of a server.
*
* @command search-api:set-index-server
*
* @usage drush search-api:set-index-server default_node_index my_solr_server
* Set the default_node_index index to used the my_solr_server server.
* @usage drush sapi-sis default_node_index my_solr_server
* Alias to set the default_node_index index to used the my_solr_server
* server.
*
* @aliases sapi-sis,search-api-set-index-server
*
* @throws \Exception
* If no index or no server were passed or passed values are invalid.
*/
public function setIndexServer($indexId, $serverId) {
$this->commandHelper->setIndexServerCommand($indexId, $serverId);
}
}
@@ -0,0 +1,8 @@
<?php
namespace Drupal\search_api;
/**
* Represents an exception that occurred in the console code of Search API.
*/
class ConsoleException extends SearchApiException {}
@@ -0,0 +1,43 @@
<?php
namespace Drupal\search_api\Contrib;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Consolidation\OutputFormatters\StructuredData\RenderCellInterface;
/**
* Outputs multi-valued data as comma-separated values.
*
* This is used in the Drush integration.
*/
class RowsOfMultiValueFields extends RowsOfFields implements RenderCellInterface {
/**
* {@inheritdoc}
*/
public function renderCell($key, $cellData, FormatterOptions $options, $rowData) {
if (is_array($cellData)) {
return static::arrayToString($cellData);
}
return $cellData;
}
/**
* Converts an array of string data into a comma separated string.
*
* @param array $array
* A multidimensional array of string data.
*
* @return string
* A comma separated string.
*/
protected static function arrayToString(array $array) {
$elements = [];
foreach ($array as $element) {
$elements[] = is_array($element) ? '"' . self::arrayToString($element) . '"' : $element;
}
return implode(',', $elements);
}
}
@@ -0,0 +1,49 @@
<?php
namespace Drupal\search_api\Contrib;
use Drupal\search_api\Plugin\views\query\SearchApiQuery;
use Drupal\views_bulk_operations\ViewsBulkOperationsEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides an event subscriber that interfaces with Views Bulk Operations.
*
* This will provide VBO integration for search views by enabling VBO to
* retrieve the entities contained in search view result rows.
*
* @see \Drupal\views_bulk_operations\EventSubscriber\ViewsBulkOperationsEventSubscriber
*/
class ViewsBulkOperationsEventSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
if (class_exists(ViewsBulkOperationsEvent::class)) {
$events[ViewsBulkOperationsEvent::NAME][] = 'provideViewData';
}
return $events;
}
/**
* Responds to view data request events.
*
* @var \Drupal\views_bulk_operations\ViewsBulkOperationsEvent $event
* The event to respond to.
*/
public function provideViewData(ViewsBulkOperationsEvent $event) {
$base_table = $event->getView()->storage->get('base_table');
$index = SearchApiQuery::getIndexFromTable($base_table);
if ($index) {
$event->setEntityTypeIds($index->getEntityTypes());
$event->setEntityGetter([
'callable' => [SearchApiQuery::class, 'getEntityFromRow'],
]);
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace Drupal\search_api\Controller;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\search_api\Task\TaskManagerInterface;
/**
* Provides an access check for the "Execute pending tasks" route.
*/
class ExecuteTasksAccessCheck implements AccessInterface {
/**
* The tasks manager service.
*
* @var \Drupal\search_api\Task\TaskManagerInterface
*/
protected $tasksManager;
/**
* Creates an ExecuteTasksAccessCheck object.
*
* @param \Drupal\search_api\Task\TaskManagerInterface $tasksManager
* The tasks manager service.
*/
public function __construct(TaskManagerInterface $tasksManager) {
$this->tasksManager = $tasksManager;
}
/**
* Checks access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access() {
// @todo Once #2722237 is fixed, see whether this can't just use the
// "search_api_task_list" cache tag instead.
if ($this->tasksManager->getTasksCount()) {
return AccessResult::allowed()->setCacheMaxAge(0);
}
return AccessResult::forbidden()->setCacheMaxAge(0);
}
}
@@ -0,0 +1,183 @@
<?php
namespace Drupal\search_api\Controller;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\RemoveCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\SearchApiException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides route responses for search indexes.
*/
class IndexController extends ControllerBase {
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack|null
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var static $controller */
$controller = parent::create($container);
$controller->setRequestStack($container->get('request_stack'));
return $controller;
}
/**
* Retrieves the request stack.
*
* @return \Symfony\Component\HttpFoundation\RequestStack
* The request stack.
*/
public function getRequestStack() {
return $this->requestStack ?: \Drupal::service('request_stack');
}
/**
* Retrieves the current request.
*
* @return \Symfony\Component\HttpFoundation\Request|null
* The current request.
*/
public function getRequest() {
return $this->getRequestStack()->getCurrentRequest();
}
/**
* Sets the request stack.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The new request stack.
*
* @return $this
*/
public function setRequestStack(RequestStack $request_stack) {
$this->requestStack = $request_stack;
return $this;
}
/**
* Displays information about a search index.
*
* @param \Drupal\search_api\IndexInterface $search_api_index
* The index to display.
*
* @return array
* An array suitable for drupal_render().
*/
public function page(IndexInterface $search_api_index) {
// Build the search index information.
$render = [
'view' => [
'#theme' => 'search_api_index',
'#index' => $search_api_index,
],
];
// Check if the index is enabled and can be written to.
if ($search_api_index->status() && !$search_api_index->isReadOnly()) {
// Attach the index status form.
$render['form'] = $this->formBuilder()->getForm('Drupal\search_api\Form\IndexStatusForm', $search_api_index);
}
return $render;
}
/**
* Returns the page title for an index's "View" tab.
*
* @param \Drupal\search_api\IndexInterface $search_api_index
* The index that is displayed.
*
* @return string
* The page title.
*/
public function pageTitle(IndexInterface $search_api_index) {
return new FormattableMarkup('@title', ['@title' => $search_api_index->label()]);
}
/**
* Enables a search index without a confirmation form.
*
* @param \Drupal\search_api\IndexInterface $search_api_index
* The index to be enabled.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response to send to the browser.
*/
public function indexBypassEnable(IndexInterface $search_api_index) {
// Enable the index.
$search_api_index->setStatus(TRUE)->save();
// \Drupal\search_api\Entity\Index::preSave() doesn't allow an index to be
// enabled if its server is not set or disabled.
if ($search_api_index->status()) {
// Notify the user about the status change.
drupal_set_message($this->t('The search index %name has been enabled.', ['%name' => $search_api_index->label()]));
}
else {
// Notify the user that the status change did not succeed.
drupal_set_message($this->t('The search index %name could not be enabled. Check if its server is set and enabled.', ['%name' => $search_api_index->label()]));
}
// Redirect to the index's "View" page.
$url = $search_api_index->toUrl('canonical');
return $this->redirect($url->getRouteName(), $url->getRouteParameters());
}
/**
* Removes a field from a search index.
*
* @param \Drupal\search_api\IndexInterface $search_api_index
* The search index.
* @param string $field_id
* The ID of the field to remove.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response to send to the browser.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the field was not found.
*/
public function removeField(IndexInterface $search_api_index, $field_id) {
$fields = $search_api_index->getFields();
$success = FALSE;
if (isset($fields[$field_id])) {
try {
$search_api_index->removeField($field_id);
$search_api_index->save();
$success = TRUE;
}
catch (SearchApiException $e) {
$args['%field'] = $fields[$field_id]->getLabel();
drupal_set_message($this->t('The field %field is locked and cannot be removed.', $args), 'error');
}
}
else {
throw new NotFoundHttpException();
}
// If this is an AJAX request, just remove the row in question.
if ($success && $this->getRequest()->request->get(AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER)) {
$response = new AjaxResponse();
$response->addCommand(new RemoveCommand("tr[data-field-row-id='$field_id']"));
return $response;
}
// Redirect to the index's "Fields" page.
$url = $search_api_index->toUrl('fields');
return $this->redirect($url->getRouteName(), $url->getRouteParameters());
}
}
@@ -0,0 +1,75 @@
<?php
namespace Drupal\search_api\Controller;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Controller\ControllerBase;
use Drupal\search_api\ServerInterface;
/**
* Provides block routines for search server-specific routes.
*/
class ServerController extends ControllerBase {
/**
* Displays information about a search server.
*
* @param \Drupal\search_api\ServerInterface $search_api_server
* The server to display.
*
* @return array
* An array suitable for drupal_render().
*/
public function page(ServerInterface $search_api_server) {
// Build the search server information.
$render = [
'view' => [
'#theme' => 'search_api_server',
'#server' => $search_api_server,
],
'#attached' => [
'library' => ['search_api/drupal.search_api.admin_css'],
],
];
// Check if the server is enabled.
if ($search_api_server->status()) {
// Attach the server status form.
$render['form'] = $this->formBuilder()->getForm('Drupal\search_api\Form\ServerStatusForm', $search_api_server);
}
return $render;
}
/**
* Returns the page title for a server's "View" tab.
*
* @param \Drupal\search_api\ServerInterface $search_api_server
* The server that is displayed.
*
* @return string
* The page title.
*/
public function pageTitle(ServerInterface $search_api_server) {
return new FormattableMarkup('@title', ['@title' => $search_api_server->label()]);
}
/**
* Enables a search server without a confirmation form.
*
* @param \Drupal\search_api\ServerInterface $search_api_server
* The server to be enabled.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response to send to the browser.
*/
public function serverBypassEnable(ServerInterface $search_api_server) {
$search_api_server->setStatus(TRUE)->save();
// Notify the user about the status change.
drupal_set_message($this->t('The search server %name has been enabled.', ['%name' => $search_api_server->label()]));
// Redirect to the server's "View" page.
$url = $search_api_server->toUrl('canonical');
return $this->redirect($url->getRouteName(), $url->getRouteParameters());
}
}
@@ -0,0 +1,65 @@
<?php
namespace Drupal\search_api\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use Drupal\search_api\Task\TaskManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Returns responses for task-related routes.
*/
class TaskController extends ControllerBase {
/**
* The server task manager.
*
* @var \Drupal\search_api\Task\TaskManagerInterface|null
*/
protected $taskManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var static $controller */
$controller = parent::create($container);
$controller->setTaskManager($container->get('search_api.task_manager'));
return $controller;
}
/**
* Retrieves the task manager.
*
* @return \Drupal\search_api\Task\TaskManagerInterface
* The task manager.
*/
public function getTaskManager() {
return $this->taskManager ?: \Drupal::service('search_api._task_manager');
}
/**
* Sets the task manager.
*
* @param \Drupal\search_api\Task\TaskManagerInterface $task_manager
* The new task manager.
*
* @return $this
*/
public function setTaskManager(TaskManagerInterface $task_manager) {
$this->taskManager = $task_manager;
return $this;
}
/**
* Executes all pending tasks.
*/
public function executeTasks() {
$this->getTaskManager()->setTasksBatch();
return batch_process(Url::fromRoute('search_api.overview'));
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\search_api\DataType;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* Defines an interface for data type plugins.
*
* @see \Drupal\search_api\Annotation\SearchApiDataType
* @see \Drupal\search_api\DataType\DataTypePluginManager
* @see \Drupal\search_api\DataType\DataTypePluginBase
* @see plugin_api
*/
interface DataTypeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface {
/**
* Returns the label of the data type.
*
* @return string
* The administration label.
*/
public function label();
/**
* Returns the description of the data type.
*/
public function getDescription();
/**
* Converts a field value to match the data type (if needed).
*
* @param mixed $value
* The value to convert.
*
* @return mixed
* The converted value.
*/
public function getValue($value);
/**
* Returns the fallback default data type for this data type.
*
* @return string
* The fallback default data type.
*/
public function getFallbackType();
/**
* Determines whether this data type is a default data type.
*
* Default data types are provided by the Search API module itself and have to
* be supported by all backends. They therefore are the only ones that can be
* used as a fallback for other data types, and don't need to have a fallback
* type themselves.
*
* @return bool
* TRUE if the data type is a default type, FALSE otherwise.
*/
public function isDefault();
}
@@ -0,0 +1,89 @@
<?php
namespace Drupal\search_api\DataType;
use Drupal\Core\Plugin\PluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a base class from which other data type classes may extend.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. These definition arrays may be altered through
* hook_search_api_data_type_info_alter(). The definition includes the following
* keys:
* - id: The unique, system-wide identifier of the data type class.
* - label: The human-readable name of the data type class, translated.
* - description: A human-readable description for the data type class,
* translated.
* - fallback_type: (optional) The fallback data type for this data type. Needs
* to be one of the default data types defined in the Search API itself.
* Defaults to "string".
*
* A complete plugin definition should be written as in this example:
*
* @code
* @SearchApiDataType(
* id = "my_data_type",
* label = @Translation("My data type"),
* description = @Translation("Some information about my data type"),
* fallback_type = "string"
* )
* @endcode
*
* Search API comes with a couple of default data types. These have an extra
* "default" property in the annotation. It is not allowed for custom data type
* plugins to set this property.
*
* @see \Drupal\search_api\Annotation\SearchApiDataType
* @see \Drupal\search_api\DataType\DataTypePluginManager
* @see \Drupal\search_api\DataType\DataTypeInterface
* @see plugin_api
*/
abstract class DataTypePluginBase extends PluginBase implements DataTypeInterface {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function getValue($value) {
return $value;
}
/**
* {@inheritdoc}
*/
public function getFallbackType() {
return !empty($this->pluginDefinition['fallback_type']) ? $this->pluginDefinition['fallback_type'] : 'string';
}
/**
* {@inheritdoc}
*/
public function isDefault() {
return !empty($this->pluginDefinition['default']);
}
/**
* {@inheritdoc}
*/
public function label() {
$plugin_definition = $this->getPluginDefinition();
return $plugin_definition['label'];
}
/**
* {@inheritdoc}
*/
public function getDescription() {
$plugin_definition = $this->getPluginDefinition();
return $plugin_definition['description'];
}
}
@@ -0,0 +1,119 @@
<?php
namespace Drupal\search_api\DataType;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages data type plugins.
*
* @see \Drupal\search_api\Annotation\SearchApiDataType
* @see \Drupal\search_api\DataType\DataTypeInterface
* @see \Drupal\search_api\DataType\DataTypePluginBase
* @see plugin_api
*/
class DataTypePluginManager extends DefaultPluginManager {
/**
* Static cache for the data type definitions.
*
* @var \Drupal\search_api\DataType\DataTypeInterface[]
*
* @see \Drupal\search_api\DataType\DataTypePluginManager::createInstance()
* @see \Drupal\search_api\DataType\DataTypePluginManager::getInstances()
*/
protected $dataTypes;
/**
* Whether all plugin instances have already been created.
*
* @var bool
*
* @see \Drupal\search_api\DataType\DataTypePluginManager::getInstances()
*/
protected $allCreated = FALSE;
/**
* Constructs a DataTypePluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/search_api/data_type', $namespaces, $module_handler, 'Drupal\search_api\DataType\DataTypeInterface', 'Drupal\search_api\Annotation\SearchApiDataType');
$this->setCacheBackend($cache_backend, 'search_api_data_type');
$this->alterInfo('search_api_data_type_info');
}
/**
* Creates or retrieves a data type plugin.
*
* @param string $plugin_id
* The ID of the plugin being instantiated.
* @param array $configuration
* (optional) An array of configuration relevant to the plugin instance.
* Ignored for data type plugins.
*
* @return \Drupal\search_api\DataType\DataTypeInterface
* The requested data type plugin.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the instance cannot be created, such as if the ID is invalid.
*/
public function createInstance($plugin_id, array $configuration = []) {
if (empty($this->dataTypes[$plugin_id])) {
$this->dataTypes[$plugin_id] = parent::createInstance($plugin_id, $configuration);
}
return $this->dataTypes[$plugin_id];
}
/**
* Returns all known data types.
*
* @return \Drupal\search_api\DataType\DataTypeInterface[]
* An array of data type plugins, keyed by type identifier.
*/
public function getInstances() {
if (!$this->allCreated) {
$this->allCreated = TRUE;
if (!isset($this->dataTypes)) {
$this->dataTypes = [];
}
foreach ($this->getDefinitions() as $plugin_id => $definition) {
if (class_exists($definition['class']) && empty($this->dataTypes[$plugin_id])) {
$data_type = $this->createInstance($plugin_id);
$this->dataTypes[$plugin_id] = $data_type;
}
}
}
return $this->dataTypes;
}
/**
* Returns all field data types known by the Search API as an options list.
*
* @return string[]
* An associative array with all recognized types as keys, mapped to their
* translated display names.
*
* @see \Drupal\search_api\DataType\DataTypePluginManager::getInstances()
*/
public function getInstancesOptions() {
$types = [];
foreach ($this->getInstances() as $id => $info) {
$types[$id] = $info->label();
}
return $types;
}
}
@@ -0,0 +1,241 @@
<?php
namespace Drupal\search_api\Datasource;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\search_api\Plugin\IndexPluginInterface;
/**
* Describes a source for search items.
*
* A datasource is used to abstract the type of data that can be indexed and
* searched with the Search API. Content entities are supported by default (with
* the \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
* datasource), but others can be added by other modules. Datasources provide
* all kinds of metadata for search items of their type, as well as loading and
* viewing functionality.
*
* Modules providing new datasources are also responsible for calling the
* appropriate track*() methods on all indexes that use that datasource when an
* item of that type is inserted, updated or deleted.
*
* Note that the two load methods in this interface do not receive the normal
* combined item IDs (that also include the datasource ID), but only the raw,
* datasource-specific IDs.
*
* @see \Drupal\search_api\Annotation\SearchApiDatasource
* @see \Drupal\search_api\Datasource\DatasourcePluginManager
* @see \Drupal\search_api\Datasource\DatasourcePluginBase
* @see plugin_api
*/
interface DatasourceInterface extends IndexPluginInterface {
/**
* Retrieves the properties exposed by the underlying complex data type.
*
* Property names have to start with a letter or an underscore, followed by
* any number of letters, numbers and underscores.
*
* @return \Drupal\Core\TypedData\DataDefinitionInterface[]
* An associative array of property data types, keyed by the property name.
*/
public function getPropertyDefinitions();
/**
* Loads an item.
*
* @param mixed $id
* The datasource-specific ID of the item.
*
* @return \Drupal\Core\TypedData\ComplexDataInterface|null
* The loaded item if it could be found, NULL otherwise.
*/
public function load($id);
/**
* Loads multiple items.
*
* @param array $ids
* An array of datasource-specific item IDs.
*
* @return \Drupal\Core\TypedData\ComplexDataInterface[]
* An associative array of loaded items, keyed by their
* (datasource-specific) IDs.
*/
public function loadMultiple(array $ids);
/**
* Retrieves the unique ID of an object from this datasource.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* An object from this datasource.
*
* @return string|null
* The datasource-internal, unique ID of the item. Or NULL if the given item
* is no valid item of this datasource.
*/
public function getItemId(ComplexDataInterface $item);
/**
* Retrieves a human-readable label for an item.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* An item of this controller's type.
*
* @return string|null
* Either a human-readable label for the item, or NULL if none is available.
*/
public function getItemLabel(ComplexDataInterface $item);
/**
* Retrieves the item's bundle.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* An item of this datasource's type.
*
* @return string
* The bundle identifier of the item. Might be just the datasource
* identifier or a similar pseudo-bundle if the datasource does not contain
* any bundles.
*
* @see getBundles()
*/
public function getItemBundle(ComplexDataInterface $item);
/**
* Retrieves the item's language.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* An item of this datasource's type.
*
* @return string
* The language code of this item.
*/
public function getItemLanguage(ComplexDataInterface $item);
/**
* Retrieves a URL at which the item can be viewed on the web.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* An item of this datasource's type.
*
* @return \Drupal\Core\Url|null
* Either an object representing the URL of the given item, or NULL if the
* item has no URL of its own.
*/
public function getItemUrl(ComplexDataInterface $item);
/**
* Checks whether a user has permission to view the given item.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* An item of this datasource's type.
* @param \Drupal\Core\Session\AccountInterface|null $account
* (optional) The user session for which to check access, or NULL to check
* access for the current user.
*
* @return bool
* TRUE if access is granted, FALSE otherwise.
*/
public function checkItemAccess(ComplexDataInterface $item, AccountInterface $account = NULL);
/**
* Returns the available view modes for this datasource.
*
* @param string|null $bundle
* (optional) The bundle for which to return the available view modes. Or
* NULL to return all view modes for this datasource, across all bundles.
*
* @return string[]
* An associative array of view mode labels, keyed by the view mode ID. Can
* be empty if it isn't possible to view items of this datasource.
*/
public function getViewModes($bundle = NULL);
/**
* Retrieves the bundles associated to this datasource.
*
* @return string[]
* An associative array mapping the datasource's bundles' IDs to their
* labels. If the datasource doesn't contain any bundles, a single
* pseudo-bundle should be returned, usually equal to the datasource
* identifier (and label).
*/
public function getBundles();
/**
* Returns the render array for the provided item and view mode.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface $item
* The item to render.
* @param string $view_mode
* (optional) The view mode that should be used to render the item.
* @param string|null $langcode
* (optional) For which language the item should be rendered. Defaults to
* the language the item has been loaded in.
*
* @return array
* A render array for displaying the item.
*/
public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL);
/**
* Returns the render array for the provided items and view mode.
*
* @param \Drupal\Core\TypedData\ComplexDataInterface[] $items
* The items to render.
* @param string $view_mode
* (optional) The view mode that should be used to render the items.
* @param string|null $langcode
* (optional) For which language the items should be rendered. Defaults to
* the language each item has been loaded in.
*
* @return array
* A render array for displaying the items.
*/
public function viewMultipleItems(array $items, $view_mode, $langcode = NULL);
/**
* Retrieves the entity type ID of items from this datasource, if any.
*
* @return string|null
* If items from this datasource are all entities of a single entity type,
* that type's ID; NULL otherwise.
*/
public function getEntityTypeId();
/**
* Returns a list of IDs of items from this datasource.
*
* Returns all items IDs by default. However, to avoid issues for large data
* sets, plugins should also implement a paging mechanism (the details of
* which are up to the datasource to decide) which guarantees that all item
* IDs can be retrieved by repeatedly calling this method with increasing
* values for $page (starting with 0) until NULL is returned.
*
* @param int|null $page
* The zero-based page of IDs to retrieve, for the paging mechanism
* implemented by this datasource; or NULL to retrieve all items at once.
*
* @return string[]|null
* An array with datasource-specific item IDs (that is, raw item IDs not
* prefixed with the datasource ID); or NULL if there are no more items for
* this and all following pages.
*/
public function getItemIds($page = NULL);
/**
* Retrieves any dependencies of the given fields.
*
* @param string[] $fields
* An array of property paths on this datasource, keyed by field IDs.
*
* @return string[][][]
* An associative array containing the dependencies of the given fields. The
* array is keyed by field ID and dependency type, the values are arrays
* with dependency names.
*/
public function getFieldDependencies(array $fields);
}
@@ -0,0 +1,158 @@
<?php
namespace Drupal\search_api\Datasource;
use Drupal\Core\Language\Language;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\search_api\Plugin\IndexPluginBase;
/**
* Defines a base class from which other datasources may extend.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. These definition arrays may be altered through
* hook_search_api_datasource_info_alter(). The definition includes the
* following keys:
* - id: The unique, system-wide identifier of the datasource.
* - label: The human-readable name of the datasource, translated.
* - description: A human-readable description for the datasource, translated.
*
* A complete plugin definition should be written as in this example:
*
* @code
* @SearchApiDatasource(
* id = "my_datasource",
* label = @Translation("My datasource"),
* description = @Translation("Exposes my custom items as a datasource."),
* )
* @endcode
*
* @see \Drupal\search_api\Annotation\SearchApiDatasource
* @see \Drupal\search_api\Datasource\DatasourcePluginManager
* @see \Drupal\search_api\Datasource\DatasourceInterface
* @see plugin_api
*/
abstract class DatasourcePluginBase extends IndexPluginBase implements DatasourceInterface {
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
return [];
}
/**
* {@inheritdoc}
*/
public function load($id) {
$items = $this->loadMultiple([$id]);
return $items ? reset($items) : NULL;
}
/**
* {@inheritdoc}
*/
public function loadMultiple(array $ids) {
return [];
}
/**
* {@inheritdoc}
*/
public function getItemLabel(ComplexDataInterface $item) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getItemBundle(ComplexDataInterface $item) {
return $this->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getItemLanguage(ComplexDataInterface $item) {
if ($item instanceof TranslatableInterface) {
return $item->language()->getId();
}
$item = $item->getValue();
if ($item instanceof TranslatableInterface) {
return $item->language()->getId();
}
return Language::LANGCODE_NOT_SPECIFIED;
}
/**
* {@inheritdoc}
*/
public function getItemUrl(ComplexDataInterface $item) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function checkItemAccess(ComplexDataInterface $item, AccountInterface $account = NULL) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getViewModes($bundle = NULL) {
return [];
}
/**
* {@inheritdoc}
*/
public function getBundles() {
return [
$this->getPluginId() => $this->label(),
];
}
/**
* {@inheritdoc}
*/
public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL) {
return [];
}
/**
* {@inheritdoc}
*/
public function viewMultipleItems(array $items, $view_mode, $langcode = NULL) {
$build = [];
foreach ($items as $key => $item) {
$build[$key] = $this->viewItem($item, $view_mode, $langcode);
}
return $build;
}
/**
* {@inheritdoc}
*/
public function getEntityTypeId() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getItemIds($page = NULL) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getFieldDependencies(array $fields) {
return [];
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\search_api\Datasource;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Manages datasource plugins.
*
* @see \Drupal\search_api\Annotation\SearchApiDatasource
* @see \Drupal\search_api\Datasource\DatasourceInterface
* @see \Drupal\search_api\Datasource\DatasourcePluginBase
* @see plugin_api
*/
class DatasourcePluginManager extends DefaultPluginManager {
/**
* Constructs a DatasourcePluginManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/search_api/datasource', $namespaces, $module_handler, 'Drupal\search_api\Datasource\DatasourceInterface', 'Drupal\search_api\Annotation\SearchApiDatasource');
$this->setCacheBackend($cache_backend, 'search_api_datasources');
$this->alterInfo('search_api_datasource_info');
}
}
@@ -0,0 +1,68 @@
<?php
namespace Drupal\search_api\Display;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A base class for display derivers.
*/
abstract class DisplayDeriverBase extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
protected $derivatives = NULL;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
$deriver = new static();
$entity_type_manager = $container->get('entity_type.manager');
$deriver->setEntityTypeManager($entity_type_manager);
$translation = $container->get('string_translation');
$deriver->setStringTranslation($translation);
return $deriver;
}
/**
* Retrieves the entity manager.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The entity manager.
*/
public function getEntityTypeManager() {
return $this->entityTypeManager;
}
/**
* Sets the entity manager.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*
* @return $this
*/
public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
return $this;
}
}

Some files were not shown because too many files have changed in this diff Show More