first import 7.x-1.4
This commit is contained in:
commit
9cc5ba4cfa
338
CHANGELOG.txt
Normal file
338
CHANGELOG.txt
Normal file
@ -0,0 +1,338 @@
|
||||
Search API 1.x, dev (xx/xx/xxxx):
|
||||
---------------------------------
|
||||
|
||||
Search API 1.4 (01/09/2013):
|
||||
----------------------------
|
||||
- #1827272 by drunken monkey: Fixed regression introduced by #1777710.
|
||||
- #1807622 by drunken monkey: Fixed definition of the default node index.
|
||||
- #1818948 by das-peter: Fixed endless loop in
|
||||
search_api_index_specific_items_delayed().
|
||||
- #1406808 by Haza, drunken monkey: Added support for date popup in exposed
|
||||
filters.
|
||||
- #1823916 by aschiwi: Fixed batch_sise typos.
|
||||
|
||||
Search API 1.3 (10/10/2012):
|
||||
----------------------------
|
||||
- Patch by mr.baileys: Fixed "enable" function doesn't use security tokens.
|
||||
- #1318904 by becw, das-peter, orakili, drunken monkey: Added improved handling
|
||||
for NULL values in Views.
|
||||
- #1306008 by Damien Tournoud, drunken monkey: Fixed handling of negative
|
||||
facets.
|
||||
- #1182912 by drunken monkey, sepgil: Added Rules action for indexing entities.
|
||||
- #1507882 by jsacksick: Added "Exclude unpublished nodes" data alteration.
|
||||
- #1225620 by drunken monkey: Added Batch API integration for the "Index now"
|
||||
functionality.
|
||||
- #1777710 by dasjo: Remove dependency on $_GET['q'] for determining base paths.
|
||||
- #1715238 by jsacksick: Fixed fulltext argument handler field list is broken.
|
||||
- #1414138 by drunken monkey: Fixed internal static index property cache.
|
||||
- #1253320 by drunken monkey, fago: Fixed improper error handling.
|
||||
|
||||
Search API 1.2 (07/07/2012):
|
||||
----------------------------
|
||||
- #1368548 by das-peter: Do not index views results by entity id.
|
||||
- #1422750 by drunken monkey, sepgil: Fixed illegal modification of entity
|
||||
objects.
|
||||
- #1363114 by nbucknor: Fixed inclusion of upper bound in range facets.
|
||||
- #1580780 by drunken monkey: Fixed default regexps of the Tokenizer.
|
||||
- #1468678 by znerol: Fixed erroneous use of Drupal multibyte wrapper functions.
|
||||
- #1600986 by DamienMcKenna: Fixed dependencies of exported search servers.
|
||||
- #1569874 by cpliakas: Fixed removal/adding of facets when indexed fields are
|
||||
changed.
|
||||
- #1528436 by jsacksick, drunken monkey: Fixed handling of exportable entities.
|
||||
|
||||
Search API 1.1 (05/23/2012):
|
||||
----------------------------
|
||||
- Fixed escaping of error messages.
|
||||
- #1330506 by drunken monkey: Removed the old Facets module.
|
||||
- #1504318 by peximo: Fixed Views pager offset.
|
||||
- #1141488 by moonray, drunken monkey: Added option to use multiple values with
|
||||
contextual filters.
|
||||
- #1535726 by bojanz, joelpittet: Fixed arguments for
|
||||
$service->configurationFormValidate() for empty forms.
|
||||
- #1400882 by mh86: Fixed "Index hierarchy" for "All parents".
|
||||
|
||||
Search API 1.0 (12/15/2011):
|
||||
----------------------------
|
||||
- #1350322 by drunken monkey: Fixed regressions introduced with cron queue
|
||||
indexing.
|
||||
- #1352292 by das-peter, drunken monkey: Use Search API specific table groups in
|
||||
Views integration.
|
||||
- #1351524 by das-peter: Made Views result row indexing more robust.
|
||||
- #1194362 by Damien Tournoud: Fixed click sort added to non-existent Views
|
||||
fields.
|
||||
- #1347150 by drunken monkey: Fixed fields access of Views facets block display.
|
||||
- #1345972 by Krasnyj, drunken monkey: Added handling of large item amounts to
|
||||
trackItemInsert().
|
||||
- #1324182 by dereine, drunken monkey: Fixed indexing author when node access is
|
||||
enabled.
|
||||
- #1215526 by cpliakas, drunken monkey: Added support for the "Bundle" facet
|
||||
dependency plugin.
|
||||
- #1337292 by drunken monkey: Fixed facet dependency system.
|
||||
|
||||
Search API 1.0, RC 1 (11/10/2011):
|
||||
----------------------------------
|
||||
API changes:
|
||||
- #1260834 by drunken monkey: Added a way to define custom data types.
|
||||
- #1308638 by drunken monkey: Reduce size of stored index settings.
|
||||
- #1291346 by drunken monkey: Expose SearchApiQuery::preExecute() and
|
||||
postExecute().
|
||||
- #955088 by dereine, drunken monkey: Provide (additional) access functionality.
|
||||
- #1064884 by drunken monkey: Added support for indexing non-entities.
|
||||
|
||||
Others:
|
||||
- #1304026 by drunken monkey: Utilize Facet API's 'include default facets' key
|
||||
in searcher definitions.
|
||||
- #1231512 by drunken monkey: Use real Relationships instead of level magic in
|
||||
Views integration.
|
||||
- #1260768 by drunken monkey: Move "Search pages" into its own project.
|
||||
- #1260812 by drunken monkey: Move "Database search" into its own project.
|
||||
- #1287602 by drunken monkey: Fixed „Index items immediately“ to delay indexing
|
||||
on insert, too.
|
||||
- #1319500 by drunken monkey: Remove items after unsuccessful loads.
|
||||
- #1297958 by drunken monkey: Fixed wrong facet operator used for range facets.
|
||||
- #1305736 by drunken monkey: Fixed notice for unset Views group operator.
|
||||
- #1263214 by drunken monkey: Fixed indexing with „Index items immediately“
|
||||
indexes old entity state.
|
||||
- #1228726 by drunken monkey, mh86: Increased size of 'options' fields in
|
||||
database.
|
||||
- #1295144 by katbailey: Added alter hook for Facet API search keys.
|
||||
- #1294828 by drunken monkey: Fixed accidental presence of good OOP coding
|
||||
standards in Views integration.
|
||||
- #1291376 by drunken monkey: Expose
|
||||
SearchApiFacetapiAdapter::getCurrentSearch().
|
||||
- #1198764 by morningtime, drunken monkey: Fixed handling of Views filter
|
||||
groups.
|
||||
- #1286500 by drunken monkey: Fixed „Search IDs” setting for facets not saved.
|
||||
- #1278780 by dereine, drunken monkey: Fixed status field requirement for node
|
||||
access.
|
||||
- #1182614 by katbailey, cpliakas, drunken monkey, thegreat, das-peter: Added
|
||||
Facet API integration.
|
||||
- #1278592 by das-peter: Fixed default view mode for non-entites or entities
|
||||
without view modes.
|
||||
- #1251674 by Nick_vh: Fixed handling of empty fulltext keys in Views.
|
||||
- #1145306 by Nick_vh, drunken monkey: Fixed handling of multiple filters in the
|
||||
database service class.
|
||||
- #1264164 by das-peter: Fixed the definition of the external data source
|
||||
controller's trackItemChange() method.
|
||||
- #1262362 by drunken monkey: Fixed error handling for orphaned facets.
|
||||
- #1233426 by atlea: Fixed dirty and queued items don't get removed from the
|
||||
tracking table when deleted.
|
||||
- #1258240 by drunken monkey: Fixed more overlooked entity type assumptions.
|
||||
- #1213698 by drunken monkey: Added a data alteration for indexing complete
|
||||
hierarchies.
|
||||
- #1252208 by tedfordgif: Fixed superfluous query chars in active facet links.
|
||||
- #1224564 by drunken monkey: Added user language as a filter in Views.
|
||||
- #1242614 by jsacksick: Fixed division by zero in drush_search_api_status().
|
||||
- #1250168 by drunken monkey: Fixed deleted items aren't removed from servers.
|
||||
- #1236642 by jsacksick, drunken monkey: Fixed stale static cache of
|
||||
search_api_get_item_type_info().
|
||||
- #1237348 by drunken monkey: Added a "Language control" data alteration.
|
||||
- #1214846 by drunken monkey, Kender: Fixed overlong table names when DB prefix
|
||||
is used.
|
||||
- #1232478 by Damien Tournoud, drunken monkey: Fixed update of field type
|
||||
settings for DB backend and index.
|
||||
- #1229772 by drunken monkey: Fixed order in which items are indexed.
|
||||
- #946624 by drunken monkey: Adapted to use a cron queue for indexing.
|
||||
- #1217702 by Amitaibu, drunken monkey: Added documentation on facet URLs.
|
||||
- #1214862 by drunken monkey: Added bundle-specific fields for related entities.
|
||||
- #1204964 by klausi: Fixed default index status is not overridden on saving.
|
||||
- #1191442 by drunken monkey: Fixed facets block view showing only tid.
|
||||
- #1161532 by drunken monkey: Fixed discerning between delete and revert in
|
||||
hook_*_delete().
|
||||
|
||||
Search API 1.0, Beta 10 (06/20/2011):
|
||||
-------------------------------------
|
||||
API changes:
|
||||
- #1068342 by drunken monkey: Added a 'fields to run on' option for processors.
|
||||
|
||||
Others:
|
||||
- #1190086 by drunken monkey: Fixed crash in hook_entity_insert().
|
||||
- #1190324 by drunken monkey: Adapted to API change in Entity API.
|
||||
- #1168684 by drunken monkey: Added improved tokenizer defaults for English.
|
||||
- #1163096 by drunken monkey: Fixed cached types for DB servers aren't correctly
|
||||
updated.
|
||||
- #1133864 by agentrickard, awolfey, greg.1.anderson, drunken monkey: Added
|
||||
Drush integration.
|
||||
|
||||
Search API 1.0, Beta 9 (06/06/2011):
|
||||
------------------------------------
|
||||
API changes:
|
||||
- #1089758 by becw, drunken monkey: Updated Views field handlers to utilize new
|
||||
features.
|
||||
- #1150260 by drunken monkey: Added a way to let processors and data alterations
|
||||
decide on which indexes they can run.
|
||||
- #1138992 by becw, drunken monkey: Added read-only indexes.
|
||||
|
||||
Others:
|
||||
- #1179990 by j0rd: Fixed facet blocks don't correctly respect the "unlimited"
|
||||
setting.
|
||||
- #1138650 by klausi, Amitaibu, drunken monkey: Fixed PHP strict warnings.
|
||||
- #1111852 by miiimooo, drunken monkey: Added a 'More like this' feature.
|
||||
- #1171360 by jbguerraz, drunken monkey: Added possibility to restrict the
|
||||
options to display in an exposed options filter.
|
||||
- #1161676 by awolfey, drunken monkey: Added Stopwords processor.
|
||||
- #1166514 by drunken monkey: Fixed parseKeys() to handle incomplete quoting.
|
||||
- #1161966 by JoeMcGuire: Added Search API Spellcheck support for Pages.
|
||||
- #1118416 by drunken monkey: Added option to index entities instantly after
|
||||
they are saved.
|
||||
- #1153298 by JoeMcGuire, drunken monkey: Added option getter and setter to
|
||||
Views query handler.
|
||||
- #1147466 by awolfey: Added excerpt Views field.
|
||||
- #1152432 by morningtime: Fixed strict warnings in render() functions.
|
||||
- #1144400 by drunken monkey: Fixed use of entity_exportable_schema_fields() in
|
||||
hook_schema().
|
||||
- #1141206 by drunken monkey: Fixed "quantity" variable for Search page pager.
|
||||
- #1117074 by drunken monkey: Fixed handling of overlong tokens by DB backend.
|
||||
- #1124548 by drunken monkey: Fixed syntax error in search_api.admin.inc.
|
||||
- #1134296 by klausi: Fixed check for NULL in SearchApiDbService->convert().
|
||||
- #1123604 by drunken monkey, fago: Added generalized "aggregation" data
|
||||
alteration.
|
||||
- #1129226 by drunken monkey: Fixed incorrect handling of facets deactivated for
|
||||
some search IDs.
|
||||
- #1086492 by drunken monkey: Fixed inadequate warnings when using the "Facets
|
||||
block" display with wrong base table.
|
||||
- #1109308 by drunken monkey : Added option to choose between display of field
|
||||
or facet name in "Current search" block.
|
||||
- #1120850 by drunken monkey, fangel: Fixed type of related entities in nested
|
||||
lists.
|
||||
|
||||
Search API 1.0, Beta 8 (04/02/2011):
|
||||
------------------------------------
|
||||
API changes:
|
||||
- #1012878 by drunken monkey: Added a way to index an entity directly.
|
||||
- #1109130 by drunken monkey: Added better structure for Views field rendering.
|
||||
|
||||
Others:
|
||||
- #1018384 by drunken monkey: Fixed Views field names to not contain colons.
|
||||
- #1105704 by drunken monkey: Fixed exposed sorts always sort on 'top' sort.
|
||||
- #1104056 by drunken monkey: Added "Current search" support for non-facet
|
||||
filters.
|
||||
- #1103814 by drunken monkey: Fixed missing argument for extractFields().
|
||||
- #1081084 by drunken monkey: Fixed notices in add_fulltext_field alteration.
|
||||
- #1091074 by drunken monkey, ygerasimov: Added machine names to "related
|
||||
entities" list.
|
||||
- #1066278 by ygerasimov, drunken monkey: Removed
|
||||
search_api_facets_by_block_status().
|
||||
- #1081666 by danielnolde: Fixed PHP notices when property labels are missing.
|
||||
|
||||
Search API 1.0, Beta 7 (03/08/2011):
|
||||
------------------------------------
|
||||
- #1083828 by drunken monkey: Added documentation on indexing custom data.
|
||||
- #1081244 by drunken monkey: Fixed debug line still contained in DB backend.
|
||||
|
||||
Search API 1.0, Beta 6 (03/04/2011):
|
||||
------------------------------------
|
||||
API changes:
|
||||
- #1075810 by drunken monkey: Added API function for marking entities as dirty.
|
||||
- #1043456 by drunken monkey: Added form validation/submission for plugins.
|
||||
- #1048032 by drunken monkey: Added a hook for altering the indexed items.
|
||||
|
||||
Others:
|
||||
- #1068334 by drunken monkey: Added a data alteration for indexing the viewed
|
||||
entity.
|
||||
- #1080376 by drunken monkey: Fixed "Current search" block field names.
|
||||
- #1076170 by drunken monkey: Added a Views display plugin for facet blocks.
|
||||
- #1064518 by drunken monkey: Added support for entity-based Views handlers.
|
||||
- #1080004 by drunken monkey: Fixed confusing "Current search" block layout.
|
||||
- #1071894 by drunken monkey: Fixed incorrect handling of boolean facets.
|
||||
- #1078590 by fago: Added check to skip default index creation when installed
|
||||
via installation profile.
|
||||
- #1018366 by drunken monkey: Added option to hide active facet links.
|
||||
- #1058410 by drunken monkey: Added overhauled display of search results.
|
||||
- #1013632 by drunken monkey: Added facet support for the database backend.
|
||||
- #1069184: "Current search" block passes query parameters wrongly.
|
||||
- #1038016 by fago: Error upon indexing inaccessible list properties.
|
||||
- #1005532: Adaption to Entity API change (new optionsList() parameter).
|
||||
- #1057224 by TimLeytens: Problem with entity_uri('file').
|
||||
- #1051286: Show type/boost options only for indexed fields.
|
||||
- #1049978: Provide a "More" link for facets.
|
||||
- #1039250: Improve facet block titles.
|
||||
- #1043492: Problems with default (exported) entities.
|
||||
- #1037916 by fago: Updates must not call API functions.
|
||||
- #1032708 by larskleiner: Notice: Undefined variable: blocks.
|
||||
- #1032404 by larskleiner: Notice: Undefined index: fields.
|
||||
- #1032032 by pillarsdotnet: search_api_enable() aborts with a database error
|
||||
on install.
|
||||
- #1026496: status doesn't get set properly when creating entities.
|
||||
- #1027992 by TimLeytens: Filter indexed items based on bundle.
|
||||
- #1024194 by TimLeytens: Provide a search block for each page.
|
||||
- #1028042: Change {search_api_item}.index_id back to an INT.
|
||||
- #1021664: Paged views results empty when adding facet.
|
||||
- #872912: Write tests.
|
||||
- #1013018: Make the "Fulltext field" data alteration more useful.
|
||||
- #1024514: Error when preprocessing muli-valued fulltext fields.
|
||||
- #1020372: CSS classes for facets.
|
||||
|
||||
Search API 1.0, Beta 5 (01/05/2011):
|
||||
------------------------------------
|
||||
API changes:
|
||||
- #917998: Enhance data alterations by making them objects.
|
||||
- #991632: Incorporate newly available entity hooks.
|
||||
- #963062: Make facets exportable.
|
||||
|
||||
Others:
|
||||
- #1018544: includes/entity.inc mentioned in a few places.
|
||||
- #1011458: Move entity and processor classes into individual files.
|
||||
- #1012478: HTML in node bodies is escaped in Views.
|
||||
- #1014548: Add newly required database fields for entities.
|
||||
- #915174: Remove unnecessary files[] declarations from .info files.
|
||||
- #988780: Merge of entity modules.
|
||||
- #997852: Service config oddities.
|
||||
- #994948: "Add index" results in blank page.
|
||||
- #993470: Unnecessary warning when no valid keys or filters are given.
|
||||
- #986412: Notice: Undefined index: value in theme_search_api_page_result().
|
||||
- #987928: EntityDBExtendable::__sleep() is gone.
|
||||
- #985324: Add "Current search" block.
|
||||
- #984174: Bug in Index::prepareProcessors() when processors have not been set.
|
||||
|
||||
Search API 1.0, Beta 4 (11/29/2010):
|
||||
------------------------------------
|
||||
API changes:
|
||||
- #976876: Move Solr module into its own project.
|
||||
- #962582: Cross-entity searches (API addition).
|
||||
- #939482 by fago: Fix exportables.
|
||||
- #939092: Several API changes regarding service class methods.
|
||||
- #939414: Enhanced service class descriptions. [soft API change]
|
||||
- #939464: Documented Entity API's module and status properties.
|
||||
- #939092: Changed private members to protected in all classes.
|
||||
- #936360: Make servers and indexes exportable.
|
||||
|
||||
Others:
|
||||
- #966512: "Time ago" option for Views date fields (+bug fix for missing value).
|
||||
- #965318: Lots of notices if entities are missing in Views.
|
||||
- #961210: Hide error messages.
|
||||
- #963756: Array to string conversion error.
|
||||
- #961276: Some random bugs.
|
||||
- #961122: Exportability UI fixes.
|
||||
- #913858: Fix adding properties that are lists of entities.
|
||||
- #961210: Don't hide error messages.
|
||||
- #961122: Display configuration status when viewing entities.
|
||||
- #889286: EntityAPIController::load() produces WSoD sometimes.
|
||||
- #958378 by marvil07: "Clear index" is broken
|
||||
- #955892: Typo in search_api_solr.install.
|
||||
- #951830: "List of language IDs" context suspicious.
|
||||
- #939414: Rename "data-alter callbacks" to "data alterations".
|
||||
- #939460: Views integration troubles.
|
||||
- #945754: Fix server and index machine name inputs.
|
||||
- #943578: Duplicate fields on service creation.
|
||||
- #709892: Invoke hook_entity_delete() on entity deletions.
|
||||
- #939414: Set fields provided by data-alter callbacks to "indexed" by default.
|
||||
- #939414: Provide a default node index upon installation.
|
||||
- #939822 by fago: Support fields.
|
||||
- #939442: Bad data type defaults [string for fields with options].
|
||||
- #939482: Override export() to work with "magic" __get fields.
|
||||
- #939442: Bad data type defaults.
|
||||
- #939414: Improved descriptions for processors.
|
||||
- #939414: Removed the "Call hook" data alter callback.
|
||||
- #938982: Not all SearchApiQuery options are passed.
|
||||
- #931066 by luke_b: HTTP timeout not set correctly.
|
||||
|
||||
Search API 1.0, Beta 3 (09/30/2010):
|
||||
------------------------------------
|
||||
- API mostly stable.
|
||||
- Five contrib modules exist:
|
||||
- search_api_db
|
||||
- search_api_solr
|
||||
- search_api_page
|
||||
- search_api_views
|
||||
- search_api_facets
|
339
LICENSE.txt
Normal file
339
LICENSE.txt
Normal file
@ -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.
|
404
README.txt
Normal file
404
README.txt
Normal file
@ -0,0 +1,404 @@
|
||||
Search API
|
||||
----------
|
||||
|
||||
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
|
||||
facetting support and the ability to use the Views module for displaying search
|
||||
results, filters, etc. Also, with the Apache Solr integration [1], a
|
||||
high-performance search engine is available for use with the Search API.
|
||||
|
||||
If you need help with the module, please post to the project's issue queue [2].
|
||||
|
||||
[1] http://drupal.org/project/search_api_solr
|
||||
[2] http://drupal.org/project/issues/search_api
|
||||
|
||||
|
||||
Content:
|
||||
- Glossary
|
||||
- Information for users
|
||||
- Information for developers
|
||||
- Included components
|
||||
|
||||
|
||||
Glossary
|
||||
--------
|
||||
|
||||
Terms as used in this module.
|
||||
|
||||
- Service class:
|
||||
A type of search engine, e.g. using the database, Apache Solr,
|
||||
Sphinx or any other professional or simple indexing mechanism. Takes care of
|
||||
the details of all operations, especially indexing or searching content.
|
||||
- Server:
|
||||
One specific place for indexing data, using a set service class. Can
|
||||
e.g. be some tables in a database, a connection to a Solr server or other
|
||||
external services, etc.
|
||||
- Index:
|
||||
A configuration object for indexing data of a specific type. What and how data
|
||||
is indexed is determined by its settings. Also keeps track of which items
|
||||
still need to be indexed (or re-indexed, if they were updated). Needs to lie
|
||||
on a server in order to be really used (although configuration is independent
|
||||
of a server).
|
||||
- Item type:
|
||||
A type of data which can be indexed (i.e., for which indexes can be created).
|
||||
Most entity types (like Content, User, Taxonomy term, etc.) are available, but
|
||||
possibly also other types provided by contrib modules.
|
||||
- Entity:
|
||||
One object of data, usually stored in the database. Might for example
|
||||
be a node, a user or a file.
|
||||
- Field:
|
||||
A defined property of an entity, like a node's title or a user's mail address.
|
||||
All fields have defined datatypes. However, for indexing purposes the user
|
||||
might choose to index a property under a different data type than defined.
|
||||
- Data type:
|
||||
Determines how a field is indexed. While "Fulltext" fields can be completely
|
||||
searched for keywords, other fields can only be used for filtering. They will
|
||||
also be converted to fit their respective value ranges.
|
||||
How types other than "Fulltext" are handled depends on the service class used.
|
||||
Its documentation should state how the type-selection affect the indexed
|
||||
content. However, service classes will always be able to handle all data
|
||||
types, it is just possible that the type doesn't affect the indexing at all
|
||||
(apart from "Fulltext vs. the rest").
|
||||
- Boost:
|
||||
Number determining how important a certain field is, when searching for
|
||||
fulltext keywords. The higher the value is, the more important is the field.
|
||||
E.g., when the node title has a boost of 5.0 and the node body a boost of 1.0,
|
||||
keywords found in the title will increase the score as much as five keywords
|
||||
found in the body. Of course, this has only an effect when the score is used
|
||||
(for sorting or other purposes). It has no effect on other parts of the search
|
||||
result.
|
||||
- Data alteration:
|
||||
A component that is used when indexing data. It can add additional fields to
|
||||
the indexed entity or prevent certain entities from being indexed. Fields
|
||||
added by callbacks have to be enabled on the "Fields" page to be of any use,
|
||||
but this is done by default.
|
||||
- Processor:
|
||||
An object that is used for preprocessing indexed data as well as search
|
||||
queries, and for postprocessing search results. Usually only work on fulltext
|
||||
fields to control how content is indexed and searched. E.g., processors can be
|
||||
used to make searches case-insensitive, to filter markup out of indexed
|
||||
content, etc.
|
||||
|
||||
|
||||
Information for users
|
||||
---------------------
|
||||
|
||||
IMPORTANT: Access checks
|
||||
In general, the Search API doesn't contain any access checks for search
|
||||
results. It is your responsibility to ensure that only accessible search
|
||||
results are displayed – either by only indexing such items, or by filtering
|
||||
appropriately at search time.
|
||||
For search on general site content (item type "Node"), this is already
|
||||
supported by the Search API. To enable this, go to the index's "Workflow" tab
|
||||
and activate the "Node access" data alteration. This will add the necessary
|
||||
field, "Node access information", to the index (which you have to leave as
|
||||
"indexed"). If both this field and "Published" are set to be indexed, access
|
||||
checks will automatically be executed at search time, showing only those
|
||||
results that a user can view. Some search types (e.g., search views) also
|
||||
provide the option to disable these access checks for individual searches.
|
||||
Please note, however, that these access checks use the indexed data, while
|
||||
usually the current data is displayed to users. Therefore, users might still
|
||||
see inappropriate content as long as items aren't indexed in their latest
|
||||
state. If you can't allow this for your site, please use the index's "Index
|
||||
immediately" feature (explained below) or possibly custom solutions for
|
||||
specific search types, if available.
|
||||
|
||||
As stated above, you will need at least one other module to use the Search API,
|
||||
namely one that defines a service class (e.g. search_api_db ("Database search"),
|
||||
provided with this module).
|
||||
|
||||
- Creating a server
|
||||
(Configuration > Search API > Add server)
|
||||
|
||||
The most basic thing you have to create is a search server for indexing content.
|
||||
Go to Configuration > Search API in the administration pages and select
|
||||
"Add server". Name and description are usually only shown to administrators and
|
||||
can be used to differentiate between several servers, or to explain a server's
|
||||
use to other administrators (for larger sites). Disabling a server makes it
|
||||
unusable for indexing and searching and can e.g. be used if the underlying
|
||||
search engine is temporarily unavailable.
|
||||
The "service class" is the most important option here, since it lets you select
|
||||
which backend the search server will use. This cannot be changed after the
|
||||
server is created.
|
||||
Depending on the selected service class, further, service-specific settings will
|
||||
be available. For details on those settings, consult the respective service's
|
||||
documentation.
|
||||
|
||||
- Creating an index
|
||||
(Configuration > Search API > Add index)
|
||||
|
||||
For adding a search index, choose "Add index" on the Search API administration
|
||||
page. Name, description and "enabled" status serve the exact same purpose as
|
||||
for servers.
|
||||
The most important option in this form is the indexed entity type. Every index
|
||||
contains data on only a single type of entities, e.g. nodes, users or taxonomy
|
||||
terms. This is therefore the only option that cannot be changed afterwards.
|
||||
The server on which the index lies determines where the data will actually be
|
||||
indexed. It doesn't affect any other settings of the index and can later be
|
||||
changed with the only drawback being that the index' content will have to be
|
||||
indexed again. You can also select a server that is at the moment disabled, or
|
||||
choose to let the index lie on no server at all, for the time being. Note,
|
||||
however, that you can only create enabled indexes on an enabled server. Also,
|
||||
disabling a server will disable all indexes that lie on it.
|
||||
The "Index items immediately" option specifies that you want items to be
|
||||
directly re-indexed after being changed, instead of waiting for the next cron
|
||||
run. Use this if it is important that users see no stale data in searches, and
|
||||
only when your setup enables relatively fast indexing.
|
||||
Lastly, the "Cron batch size" option allows you to set whether items will be
|
||||
indexed when cron runs (as long as the index is enabled), and how many items
|
||||
will be indexed in a single batch. The best value for this setting depends on
|
||||
how time-consuming indexing is for your setup, which in turn depends mostly on
|
||||
the server used and the enabled data alterations. You should set it to a number
|
||||
of items which can easily be indexed in 10 seconds' time. Items can also be
|
||||
indexed manually, or directly when they are changed, so even if this is set to
|
||||
0, the index can still be used.
|
||||
|
||||
- Indexed fields
|
||||
(Configuration > Search API > [Index name] > Fields)
|
||||
|
||||
Here you can select which of the entities' fields will be indexed, and how.
|
||||
Fields added by (enabled) data alterations will be available here, too.
|
||||
Without selecting fields to index, the index will be useless and also won't be
|
||||
available for searches. Select the "Fulltext" data type for fields which you
|
||||
want search for keywords, and other data types when you want to use the field
|
||||
for filtering (e.g., as facets). The "Item language" field will always be
|
||||
indexed as it contains important information for processors and hooks.
|
||||
You can also add fields of related entities here, via the "Add related fields"
|
||||
form at the bottom of the page. For instance, you might want to index the
|
||||
author's username to the indexed data of a node, and you need to add the "Body"
|
||||
entity to the node when you want to index the actual text it contains.
|
||||
|
||||
- Index workflow
|
||||
(Configuration > Search API > [Index name] > Workflow)
|
||||
|
||||
This page lets you customize how the created index works, and what metadata will
|
||||
be available, by selecting data alterations and processors (see the glossary for
|
||||
further explanations).
|
||||
Data alterations usually only add one or more fields to the entity and their
|
||||
order is mostly irrelevant.
|
||||
The order of processors, however, often is important. Read the processors'
|
||||
descriptions or consult their documentation for determining how to use them most
|
||||
effectively.
|
||||
|
||||
- Index status
|
||||
(Configuration > Search API > [Index name] > Status)
|
||||
|
||||
On this page you can view how much of the entities are already indexed and also
|
||||
control indexing. With the "Index now" button (displayed only when there are
|
||||
still unindexed items) you can directly index a certain number of "dirty" items
|
||||
(i.e., items not yet indexed in their current state). Setting "-1" as the number
|
||||
will index all of those items, similar to the cron batch size setting.
|
||||
When you change settings that could affect indexing, and the index is not
|
||||
automatically marked for re-indexing, you can do this manually with the
|
||||
"Re-index content" button. All items in the index will be marked as dirty and be
|
||||
re-indexed when subsequently indexing items (either manually or via cron runs).
|
||||
Until all content is re-indexed, the old data will still show up in searches.
|
||||
This is different with the "Clear index" button. All items will be marked as
|
||||
dirty and additionally all data will be removed from the index. Therefore,
|
||||
searches won't show any results until items are re-indexed, after clearing an
|
||||
index. Use this only if completely wrong data has been indexed. It is also done
|
||||
automatically when the index scheme or server settings change too drastically to
|
||||
keep on using the old data.
|
||||
|
||||
- Hidden settings
|
||||
|
||||
search_api_index_worker_callback_runtime:
|
||||
By changing this variable, you can determine the time (in seconds) the Search
|
||||
API will spend indexing (for all indexes combined) in each cron run. The
|
||||
default is 15 seconds.
|
||||
|
||||
|
||||
Information for developers
|
||||
--------------------------
|
||||
|
||||
| NOTE:
|
||||
| For modules providing new entities: In order for your entities to become
|
||||
| searchable with the Search API, your module will need to implement
|
||||
| hook_entity_property_info() in addition to the normal hook_entity_info().
|
||||
| hook_entity_property_info() is documented in the entity module.
|
||||
| For making certain non-entities searchable, see "Item type" below.
|
||||
| For custom field types to be available for indexing, provide a
|
||||
| "property_type" key in hook_field_info(), and optionally a callback at the
|
||||
| "property_callbacks" key.
|
||||
| Both processes are explained in [1].
|
||||
|
|
||||
| [1] http://drupal.org/node/1021466
|
||||
|
||||
Apart from improving the module itself, developers can extend search
|
||||
capabilities provided by the Search API by providing implementations for one (or
|
||||
several) of the following classes. Detailed documentation on the methods that
|
||||
need to be implemented are always available as doc comments in the respective
|
||||
interface definition (all found in their respective files in the includes/
|
||||
directory). The details for hooks can be looked up in the search_api.api.php
|
||||
file.
|
||||
For all interfaces there are handy base classes which can (but don't need to) be
|
||||
used to ease custom implementations, since they provide sensible generic
|
||||
implementations for many methods. They, too, should be documented well enough
|
||||
with doc comments for a developer to find the right methods to override or
|
||||
implement.
|
||||
|
||||
- Service class
|
||||
Interface: SearchApiServiceInterface
|
||||
Base class: SearchApiAbstractService
|
||||
Hook: hook_search_api_service_info()
|
||||
|
||||
The service classes are the heart of the API, since they allow data to be
|
||||
indexed on different search servers. Since these are quite some work to get
|
||||
right, you should probably make sure a service class for a specific search
|
||||
engine doesn't exist already before programming it yourself.
|
||||
When your module supplies a service class, please make sure to provide
|
||||
documentation (at least a README.txt) that clearly states the datatypes it
|
||||
supports (and in what manner), how a direct query (a query where the keys are
|
||||
a single string, instead of an array) is parsed and possible limitations of the
|
||||
service class.
|
||||
The central methods here are the indexItems() and the search() methods, which
|
||||
always have to be overridden manually. The configurationForm() method allows
|
||||
services to provide custom settings for the user.
|
||||
See the SearchApiDbService class for an example implementation.
|
||||
|
||||
- Query class
|
||||
Interface: SearchApiQueryInterface
|
||||
Base class: SearchApiQuery
|
||||
|
||||
You can also override the query class' behaviour for your service class. You
|
||||
can, for example, change key parsing behaviour, add additional parse modes
|
||||
specific to your service, or override methods so the information is stored more
|
||||
suitable for your service.
|
||||
For the query class to become available (other than through manual creation),
|
||||
you need a custom service class where you override the query() method to return
|
||||
an instance of your query class.
|
||||
|
||||
- Item type
|
||||
Interface: SearchApiDataSourceControllerInterface
|
||||
Base class: SearchApiAbstractDataSourceController
|
||||
Hook: hook_search_api_item_type_info()
|
||||
|
||||
If you want to index some data which is not defined as an entity, you can
|
||||
specify it as a new item type here. For defining a new item type, you have to
|
||||
create a data source controller for the type and track new, changed and deleted
|
||||
items of the type by calling the search_api_track_item_*() functions.
|
||||
An instance of the data source controller class will then be used by indexes
|
||||
when handling items of your newly-defined type.
|
||||
|
||||
If you want to make external data that is indexed on some search server
|
||||
available to the Search API, there is a handy base class for your data source
|
||||
controller (SearchApiExternalDataSourceController in
|
||||
includes/datasource_external.inc) which you can extend. For a minimal use case,
|
||||
you will then only have to define the available fields that can be retrieved by
|
||||
the server.
|
||||
|
||||
- Data type
|
||||
Hook: hook_search_api_data_type_info()
|
||||
|
||||
You can specify new data types for indexing fields. These new types can then be
|
||||
selected on indexes' „Fields“ tabs. You just have to implement the hook,
|
||||
returning some information on your data type, and specify in your module's
|
||||
documentation the format of your data type and how it should be used.
|
||||
|
||||
For a custom data type to have an effect, in most cases the server's service
|
||||
class has to support that data type. A service class can advertize its support
|
||||
of a data type by declaring support for the "search_api_data_type_TYPE" feature
|
||||
in its supportsFeature() method. If this support isn't declared, a fallback data
|
||||
type is automatically used instead of the custom one.
|
||||
|
||||
If a field is indexed with a custom data type, its entry in the index's options
|
||||
array will have the selected type in "real_type", while "type" contains the
|
||||
fallback type (which is always one of the default data types, as returned by
|
||||
search_api_default_field_types().
|
||||
|
||||
- Data-alter callbacks
|
||||
Interface: SearchApiAlterCallbackInterface
|
||||
Base class: SearchApiAbstractAlterCallback
|
||||
Hook: hook_search_api_alter_callback_info()
|
||||
|
||||
Data alter callbacks can be used to change the field data of indexed items, or
|
||||
to prevent certain items from being indexed. They are only used when indexing,
|
||||
or when selecting the fields to index. For adding additional information to
|
||||
search results, you have to use a processor.
|
||||
Data-alter callbacks are called "data alterations" in the UI.
|
||||
|
||||
- Processors
|
||||
Interface: SearchApiProcessorInterface
|
||||
Base class: SearchApiAbstractProcessor
|
||||
Hook: hook_search_api_processor_info()
|
||||
|
||||
Processors are used for altering the data when indexing or searching. The exact
|
||||
specifications are available in the interface's doc comments. Just note that the
|
||||
processor description should clearly state assumptions or restrictions on input
|
||||
types (e.g. only tokenized text), item language, etc. and explain concisely what
|
||||
effect it will have on searches.
|
||||
See the processors in includes/processor.inc for examples.
|
||||
|
||||
|
||||
Included components
|
||||
-------------------
|
||||
|
||||
- Service classes
|
||||
|
||||
* Database search
|
||||
A search server implementation that uses the normal database for indexing
|
||||
data. It isn't very fast and the results might also be less accurate than
|
||||
with third-party solutions like Solr, but it's very easy to set up and good
|
||||
for smaller applications or testing.
|
||||
See contrib/search_api_db/README.txt for details.
|
||||
|
||||
- Data alterations
|
||||
|
||||
* URL field
|
||||
Provides a field with the URL for displaying the entity.
|
||||
* Aggregated fields
|
||||
Offers the ability to add additional fields to the entity, containing the
|
||||
data from one or more other fields. Use this, e.g., to have a single field
|
||||
containing all data that should be searchable, or to make the text from a
|
||||
string field, like a taxonomy term, also fulltext-searchable.
|
||||
The type of aggregation can be selected from a set of values: you can, e.g.,
|
||||
collect the text data of all contained fields, or add them up, count their
|
||||
values, etc.
|
||||
* Bundle filter
|
||||
Enables the admin to prevent entities from being indexed based on their
|
||||
bundle (content type for nodes, vocabulary for taxonomy terms, etc.).
|
||||
* Complete entity view
|
||||
Adds a field containing the whole HTML content of the entity as it is viewed
|
||||
on the site. The view mode used can be selected.
|
||||
Note, however, that this might not work for entities of all types. All core
|
||||
entities except files are supported, though.
|
||||
* Index hierarchy
|
||||
Allows to index a hierarchical field along with all its parents. Most
|
||||
importantly, this can be used to index taxonomy term references along with
|
||||
all parent terms. This way, when an item, e.g., has the term "New York", it
|
||||
will also be matched when filtering for "USA" or "North America".
|
||||
|
||||
- Processors
|
||||
|
||||
* Ignore case
|
||||
Makes all fulltext searches (and, optionally, also filters on string values)
|
||||
case-insensitive. Some servers might do this automatically, for others this
|
||||
should probably always be activated.
|
||||
* HTML filter
|
||||
Strips HTML tags from fulltext fields and decodes HTML entities. If you are
|
||||
indexing HTML content (like node bodies) and the search server doesn't
|
||||
handle HTML on its own, this should be activated to avoid indexing HTML
|
||||
tags, as well as to give e.g. terms appearing in a heading a higher boost.
|
||||
* Tokenizer
|
||||
This processor allows you to specify how indexed fulltext content is split
|
||||
into seperate tokens – which characters are ignored and which treated as
|
||||
white-space that seperates words.
|
||||
* Stopwords
|
||||
Enables the admin to specify a stopwords file, the words contained in which
|
||||
will be filtered out of the text data indexed. This can be used to exclude
|
||||
too common words from indexing, for servers not supporting this natively.
|
||||
|
||||
- Additional modules
|
||||
|
||||
* Search pages
|
||||
This module lets you create simple search pages for indexes.
|
||||
* Search views
|
||||
This integrates the Search API with the Views module [1], enabling the user
|
||||
to create views which display search results from any Search API index.
|
||||
* Search facets
|
||||
For service classes supporting this feature (e.g. Solr search), this module
|
||||
automatically provides configurable facet blocks on pages that execute
|
||||
a search query.
|
||||
|
||||
[1] http://drupal.org/project/views
|
125
contrib/search_api_facetapi/README.txt
Normal file
125
contrib/search_api_facetapi/README.txt
Normal file
@ -0,0 +1,125 @@
|
||||
Search facets
|
||||
-------------
|
||||
|
||||
This module allows you to create facetted searches for any search executed via
|
||||
the Search API, no matter if executed by a search page, a view or any other
|
||||
module. The only thing you'll need is a search service class that supports the
|
||||
"search_api_facets" feature. Currently, the "Database search" and "Solr search"
|
||||
modules supports this.
|
||||
|
||||
This module is built on the Facet API [1], which is needed for this module to
|
||||
work.
|
||||
|
||||
[1] http://drupal.org/project/facetapi
|
||||
|
||||
|
||||
Information for site builders
|
||||
-----------------------------
|
||||
|
||||
For creating a facetted search, you first need a search. Create or find some
|
||||
page displaying Search API search results, either via a search page, a view or
|
||||
by any other means. Now go to the configuration page for the index on which
|
||||
this search is executed.
|
||||
If the index lies on a server supporting facets (and if this module is enabled),
|
||||
you'll notice a "Facets" tab. Click it and it will take you to the index' facet
|
||||
configuration page. You'll see a table containing all indexed fields and options
|
||||
for enabling and configuring facets for them.
|
||||
For a detailed explanation of the available options, please refer to the Facet
|
||||
API documentation.
|
||||
|
||||
- Creating facets via the URL
|
||||
|
||||
Facets can be added to a search (for which facets are activated) by passing
|
||||
appropriate GET parameters in the URL. Assuming you have an indexed field with
|
||||
the machine name "field_price", you can filter on it in the following ways:
|
||||
|
||||
- Filter for a specific value. For finding only results that have a price of
|
||||
exactly 100, pass the following $options to url() or l():
|
||||
|
||||
$options['query']['f'][] = 'field_price:100';
|
||||
|
||||
Or manually append the following GET parameter to a URL:
|
||||
|
||||
?f[0]=field_price:100
|
||||
|
||||
- Search for values in a specified range. The following example will only return
|
||||
items that have a price greater than or equal to 100 and lower than 500.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[100 TO 500]';
|
||||
URL: ?f[0]=field_price%3A%5B100%20TO%20500%5D
|
||||
|
||||
- Search for values above a value. The next example will find results which have
|
||||
a price greater than or equal to 100. The asterisk (*) stands for "unlimited",
|
||||
meaning that there is no upper limit. Filtering for values lower than a
|
||||
certain value works equivalently.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[100 TO *]';
|
||||
URL: ?f[0]=field_price%3A%5B100%20TO%20%2A%5D
|
||||
|
||||
- Search for missing values. This example will filter out all items which have
|
||||
any value at all in the price field, and will therefore only list items on
|
||||
which this field was omitted. (This naturally only makes sense for fields
|
||||
that aren't required.)
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:!';
|
||||
URL: ?f[0]=field_price%3A%21
|
||||
|
||||
- Search for present values. The following example will only return items which
|
||||
have the price field set (regardless of the actual value). You can see that it
|
||||
is actually just a range filter with unlimited lower and upper bound.
|
||||
|
||||
Code: $options['query']['f'][] = 'field_price:[* TO *]';
|
||||
URL: ?f[0]=field_price%3A%5B%2A%20TO%20%2A%5D
|
||||
|
||||
Note: When filtering a field whose machine name contains a colon (e.g.,
|
||||
"author:roles"), you'll have to additionally URL-encode the field name in these
|
||||
filter values:
|
||||
Code: $options['query']['f'][] = rawurlencode('author:roles') . ':100';
|
||||
URL: ?f[0]=author%253Aroles%3A100
|
||||
|
||||
- Issues
|
||||
|
||||
If you find any bugs or shortcomings while using this module, please file an
|
||||
issue in the project's issue queue [1], using the "Facets" component.
|
||||
|
||||
[1] http://drupal.org/project/issues/search_api
|
||||
|
||||
|
||||
Information for developers
|
||||
--------------------------
|
||||
|
||||
- Features
|
||||
|
||||
If you are the developer of a SearchApiServiceInterface implementation and want
|
||||
to support facets with your service class, too, you'll have to support the
|
||||
"search_api_facets" feature. You can find details about the necessary additions
|
||||
to your class in the example_servive.php file. In short, you'll just, when
|
||||
executing a query, have to return facet terms and counts according to the
|
||||
query's "search_api_facets" option, if present.
|
||||
In order for the module to be able to tell that your server supports facets,
|
||||
you will also have to change your service's supportsFeature() method to
|
||||
something like the following:
|
||||
public function supportsFeature($feature) {
|
||||
return $feature == 'search_api_facets';
|
||||
}
|
||||
|
||||
There is also a second feature defined by this module, namely
|
||||
"search_api_facets_operator_or", for supporting "OR" facets. The requirements
|
||||
for this feature are also explained in the example_servive.php file.
|
||||
|
||||
- Query option
|
||||
|
||||
The facets created follow the "search_api_base_path" option on the search query.
|
||||
If set, this path will be used as the base path from which facet links will be
|
||||
created. This can be used to show facets on pages without searches – e.g., as a
|
||||
landing page.
|
||||
|
||||
- Hidden variable
|
||||
|
||||
The module uses one hidden variable, "search_api_facets_search_ids", to keep
|
||||
track of the search IDs of searches executed for a given index. It is only
|
||||
updated when a facet is displayed for the respective search, so isn't really a
|
||||
reliable measure for this.
|
||||
In any case, if you e.g. did some test searches and now don't want them to show
|
||||
up in the block configuration forever after, just clear the variable:
|
||||
variable_del("search_api_facets_search_ids")
|
209
contrib/search_api_facetapi/example_service.php
Normal file
209
contrib/search_api_facetapi/example_service.php
Normal file
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Example implementation for a service class which supports facets.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Example class explaining how facets can be supported by a service class.
|
||||
*
|
||||
* This class defines the "search_api_facets" and
|
||||
* "search_api_facets_operator_or" features. Read the method documentation and
|
||||
* inline comments in search() to learn how they can be supported by a service
|
||||
* class.
|
||||
*/
|
||||
abstract class SearchApiFacetapiExampleService extends SearchApiAbstractService {
|
||||
|
||||
/**
|
||||
* Determines whether this service class implementation supports a given
|
||||
* feature. Features are optional extensions to Search API functionality and
|
||||
* usually defined and used by third-party modules.
|
||||
*
|
||||
* If the service class supports facets, it should return TRUE if called with
|
||||
* the feature name "search_api_facets". If it also supports "OR" facets, it
|
||||
* should also return TRUE if called with "search_api_facets_operator_or".
|
||||
*
|
||||
* @param string $feature
|
||||
* The name of the optional feature.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
$supported = array(
|
||||
'search_api_facets' => TRUE,
|
||||
'search_api_facets_operator_or' => TRUE,
|
||||
);
|
||||
return isset($supported[$feature]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a search on the server represented by this object.
|
||||
*
|
||||
* If the service class supports facets, it should check for an additional
|
||||
* option on the query object:
|
||||
* - search_api_facets: An array of facets to return along with the results
|
||||
* for this query. The array is keyed by an arbitrary string which should
|
||||
* serve as the facet's unique identifier for this search. The values are
|
||||
* arrays with the following keys:
|
||||
* - field: The field to construct facets for.
|
||||
* - limit: The maximum number of facet terms to return. 0 or an empty
|
||||
* value means no limit.
|
||||
* - min_count: The minimum number of results a facet value has to have in
|
||||
* order to be returned.
|
||||
* - missing: If TRUE, a facet for all items with no value for this field
|
||||
* should be returned (if it conforms to limit and min_count).
|
||||
* - operator: (optional) If the service supports "OR" facets and this key
|
||||
* contains the string "or", the returned facets should be "OR" facets. If
|
||||
* the server doesn't support "OR" facets, this key can be ignored.
|
||||
*
|
||||
* The basic principle of facets is explained quite well in the
|
||||
* @link http://en.wikipedia.org/wiki/Faceted_search Wikipedia article on
|
||||
* "Faceted search" @endlink. Basically, you should return for each field
|
||||
* filter values which would yield some results when used with the search.
|
||||
* E.g., if you return for a field $field the term $term with $count results,
|
||||
* the given $query along with
|
||||
* $query->condition($field, $term)
|
||||
* should yield exactly (or about) $count results.
|
||||
*
|
||||
* For "OR" facets, all existing filters on the facetted field should be
|
||||
* ignored for computing the facets.
|
||||
*
|
||||
* @param $query
|
||||
* The SearchApiQueryInterface object to execute.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as required by
|
||||
* SearchApiQueryInterface::execute().
|
||||
* In addition, if the "search_api_facets" option is present on the query,
|
||||
* the results should contain an array of facets in the "search_api_facets"
|
||||
* key, as specified by the option. The facets array should be keyed by the
|
||||
* facets' unique identifiers, and contain a numeric array of facet terms,
|
||||
* sorted descending by result count. A term is represented by an array with
|
||||
* the following keys:
|
||||
* - count: Number of results for this term.
|
||||
* - filter: The filter to apply when selecting this facet term. A filter is
|
||||
* a string of one of the following forms:
|
||||
* - "VALUE": Filter by the literal value VALUE (always include the
|
||||
* quotes, not only for strings).
|
||||
* - [VALUE1 VALUE2]: Filter for a value between VALUE1 and VALUE2. Use
|
||||
* parantheses for excluding the border values and square brackets for
|
||||
* including them. An asterisk (*) can be used as a wildcard. E.g.,
|
||||
* (* 0) or [* 0) would be a filter for all negative values.
|
||||
* - !: Filter for items without a value for this field (i.e., the
|
||||
* "missing" facet).
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error prevented the search from completing.
|
||||
*/
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
// We assume here that we have an AI search which understands English
|
||||
// commands.
|
||||
|
||||
// First, create the normal search query, without facets.
|
||||
$search = new SuperCoolAiSearch($query->getIndex());
|
||||
$search->cmd('create basic search for the following query', $query);
|
||||
$ret = $search->cmd('return search results in Search API format');
|
||||
|
||||
// Then, let's see if we should return any facets.
|
||||
if ($facets = $query->getOption('search_api_facets')) {
|
||||
// For the facets, we need all results, not only those in the specified
|
||||
// range.
|
||||
$results = $search->cmd('return unlimited search results as a set');
|
||||
foreach ($facets as $id => $facet) {
|
||||
$field = $facet['field'];
|
||||
$limit = empty($facet['limit']) ? 'all' : $facet['limit'];
|
||||
$min_count = $facet['min_count'];
|
||||
$missing = $facet['missing'];
|
||||
$or = isset($facet['operator']) && $facet['operator'] == 'or';
|
||||
|
||||
// If this is an "OR" facet, existing filters on the field should be
|
||||
// ignored for computing the facets.
|
||||
// You can ignore this if your service class doesn't support the
|
||||
// "search_api_facets_operator_or" feature.
|
||||
if ($or) {
|
||||
// We have to execute another query (in the case of this hypothetical
|
||||
// search backend, at least) to get the right result set to facet.
|
||||
$tmp_search = new SuperCoolAiSearch($query->getIndex());
|
||||
$tmp_search->cmd('create basic search for the following query', $query);
|
||||
$tmp_search->cmd("remove all conditions for field $field");
|
||||
$tmp_results = $tmp_search->cmd('return unlimited search results as a set');
|
||||
}
|
||||
else {
|
||||
// Otherwise, we can just use the normal results.
|
||||
$tmp_results = $results;
|
||||
}
|
||||
|
||||
$filters = array();
|
||||
if ($search->cmd("$field is a date or numeric field")) {
|
||||
// For date, integer or float fields, range facets are more useful.
|
||||
$ranges = $search->cmd("list $limit ranges of field $field in the following set", $tmp_results);
|
||||
foreach ($ranges as $range) {
|
||||
if ($range->getCount() >= $min_count) {
|
||||
// Get the lower and upper bound of the range. * means unlimited.
|
||||
$lower = $range->getLowerBound();
|
||||
$lower = ($lower == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $lower;
|
||||
$upper = $range->getUpperBound();
|
||||
$upper = ($upper == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $upper;
|
||||
// Then, see whether the bounds are included in the range. These
|
||||
// can be specified independently for the lower and upper bound.
|
||||
// Parentheses are used for exclusive bounds, square brackets are
|
||||
// used for inclusive bounds.
|
||||
$lowChar = $range->isLowerBoundInclusive() ? '[' : '(';
|
||||
$upChar = $range->isUpperBoundInclusive() ? ']' : ')';
|
||||
// Create the filter, which separates the bounds with a single
|
||||
// space.
|
||||
$filter = "$lowChar$lower $upper$upChar";
|
||||
$filters[$filter] = $range->getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Otherwise, we use normal single-valued facets.
|
||||
$terms = $search->cmd("list $limit values of field $field in the following set", $tmp_results);
|
||||
foreach ($terms as $term) {
|
||||
if ($term->getCount() >= $min_count) {
|
||||
// For single-valued terms, we just need to wrap them in quotes.
|
||||
$filter = '"' . $term->getValue() . '"';
|
||||
$filters[$filter] = $term->getCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we should also return a "missing" facet, compute that as the
|
||||
// number of results without a value for the facet field.
|
||||
if ($missing) {
|
||||
$count = $search->cmd("return number of results without field $field in the following set", $tmp_results);
|
||||
if ($count >= $min_count) {
|
||||
$filters['!'] = $count;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the facets descending by result count.
|
||||
arsort($filters);
|
||||
|
||||
// With the "missing" facet, we might have too many facet terms (unless
|
||||
// $limit was empty and is therefore now set to "all"). If this is the
|
||||
// case, remove those with the lowest number of results.
|
||||
while (is_numeric($limit) && count($filters) > $limit) {
|
||||
array_pop($filters);
|
||||
}
|
||||
|
||||
// Now add the facet terms to the return value, as specified in the doc
|
||||
// comment for this method.
|
||||
foreach ($filters as $filter => $count) {
|
||||
$ret['search_api_facets'][$id][] = array(
|
||||
'count' => $count,
|
||||
'filter' => $filter,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the results, which now also includes the facet information.
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
242
contrib/search_api_facetapi/plugins/facetapi/adapter.inc
Normal file
242
contrib/search_api_facetapi/plugins/facetapi/adapter.inc
Normal file
@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Classes used by the Facet API module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Facet API adapter for the Search API module.
|
||||
*/
|
||||
class SearchApiFacetapiAdapter extends FacetapiAdapter {
|
||||
|
||||
/**
|
||||
* Cached value for the current search for this searcher, if any.
|
||||
*
|
||||
* @see getCurrentSearch()
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $current_search;
|
||||
|
||||
/**
|
||||
* The active facet fields for the current search.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = array();
|
||||
|
||||
/**
|
||||
* Returns the path to the admin settings for a given realm.
|
||||
*
|
||||
* @param $realm_name
|
||||
* The name of the realm.
|
||||
*
|
||||
* @return
|
||||
* The path to the admin settings.
|
||||
*/
|
||||
public function getPath($realm_name) {
|
||||
$base_path = 'admin/config/search/search_api';
|
||||
$index_id = $this->info['instance'];
|
||||
return $base_path . '/index/' . $index_id . '/facets/' . $realm_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides FacetapiAdapter::getSearchPath().
|
||||
*/
|
||||
public function getSearchPath() {
|
||||
$search = $this->getCurrentSearch();
|
||||
if ($search && $search[0]->getOption('search_api_base_path')) {
|
||||
return $search[0]->getOption('search_api_base_path');
|
||||
}
|
||||
return $_GET['q'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the backend to initialize its query object before adding the facet filters.
|
||||
*
|
||||
* @param mixed $query
|
||||
* The backend's native object.
|
||||
*/
|
||||
public function initActiveFilters($query) {
|
||||
$search_id = $query->getOption('search id');
|
||||
$index_id = $this->info['instance'];
|
||||
$facets = facetapi_get_enabled_facets($this->info['name']);
|
||||
$this->fields = array();
|
||||
|
||||
// We statically store the current search per facet so that we can correctly
|
||||
// assign it when building the facets. See the build() method in the query
|
||||
// type plugin classes.
|
||||
$active = &drupal_static('search_api_facetapi_active_facets', array());
|
||||
foreach ($facets as $facet) {
|
||||
$options = $this->getFacet($facet)->getSettings()->settings;
|
||||
// The 'default_true' option is a choice between "show on all but the
|
||||
// selected searches" (TRUE) and "show for only the selected searches".
|
||||
$default_true = isset($options['default_true']) ? $options['default_true'] : TRUE;
|
||||
// The 'facet_search_ids' option is the list of selected searches that
|
||||
// will either be excluded or for which the facet will exclusively be
|
||||
// displayed.
|
||||
$facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array();
|
||||
|
||||
if (array_search($search_id, $facet_search_ids) === FALSE) {
|
||||
$search_ids = variable_get('search_api_facets_search_ids', array());
|
||||
if (empty($search_ids[$index_id][$search_id])) {
|
||||
// Remember this search ID.
|
||||
$search_ids[$index_id][$search_id] = $search_id;
|
||||
variable_set('search_api_facets_search_ids', $search_ids);
|
||||
}
|
||||
if (!$default_true) {
|
||||
continue; // We are only to show facets for explicitly named search ids.
|
||||
}
|
||||
}
|
||||
elseif ($default_true) {
|
||||
continue; // The 'facet_search_ids' in the settings are to be excluded.
|
||||
}
|
||||
$active[$facet['name']] = $search_id;
|
||||
$this->fields[$facet['name']] = array(
|
||||
'field' => $facet['field'],
|
||||
'limit' => $options['hard_limit'],
|
||||
'operator' => $options['operator'],
|
||||
'min_count' => $options['facet_mincount'],
|
||||
'missing' => $options['facet_missing'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given facet to the query.
|
||||
*/
|
||||
public function addFacet(array $facet, SearchApiQueryInterface $query) {
|
||||
if (isset($this->fields[$facet['name']])) {
|
||||
$options = &$query->getOptions();
|
||||
$options['search_api_facets'][$facet['name']] = $this->fields[$facet['name']];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean flagging whether $this->_searcher executed a search.
|
||||
*/
|
||||
public function searchExecuted() {
|
||||
return (bool) $this->getCurrentSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for getting a current search for this searcher.
|
||||
*
|
||||
* @return array
|
||||
* The first matching current search, in the form specified by
|
||||
* search_api_current_search(). Or NULL, if no match was found.
|
||||
*/
|
||||
public function getCurrentSearch() {
|
||||
if (!isset($this->current_search)) {
|
||||
$this->current_search = FALSE;
|
||||
$index_id = $this->info['instance'];
|
||||
// There is currently no way to configure the "current search" block to
|
||||
// show on a per-searcher basis as we do with the facets. Therefore we
|
||||
// cannot match it up to the correct "current search".
|
||||
// I suspect that http://drupal.org/node/593658 would help.
|
||||
// For now, just taking the first current search for this index. :-/
|
||||
foreach (search_api_current_search() as $search) {
|
||||
list($query, $results) = $search;
|
||||
if ($query->getIndex()->machine_name == $index_id) {
|
||||
$this->current_search = $search;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->current_search ? $this->current_search : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean flagging whether facets in a realm shoud be displayed.
|
||||
*
|
||||
* Useful, for example, for suppressing sidebar blocks in some cases.
|
||||
*
|
||||
* @return
|
||||
* A boolean flagging whether to display a given realm.
|
||||
*/
|
||||
public function suppressOutput($realm_name) {
|
||||
// Not sure under what circumstances the output will need to be suppressed?
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search keys.
|
||||
*/
|
||||
public function getSearchKeys() {
|
||||
$search = $this->getCurrentSearch();
|
||||
$keys = $search[0]->getOriginalKeys();
|
||||
if (is_array($keys)) {
|
||||
// This will happen nearly never when displaying the search keys to the
|
||||
// user, so go with a simple work-around.
|
||||
// If someone complains, we can easily add a method for printing them
|
||||
// properly.
|
||||
$keys = '[' . t('complex query') . ']';
|
||||
}
|
||||
elseif (!$keys) {
|
||||
// If a base path other than the current one is set, we assume that we
|
||||
// shouldn't report on the current search. Highly hack-y, of course.
|
||||
if ($search[0]->getOption('search_api_base_path', $_GET['q']) !== $_GET['q']) {
|
||||
return NULL;
|
||||
}
|
||||
// Work-around since Facet API won't show the "Current search" block
|
||||
// without keys.
|
||||
$keys = '[' . t('all items') . ']';
|
||||
}
|
||||
drupal_alter('search_api_facetapi_keys', $keys, $search[0]);
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of total results found for the current search.
|
||||
*/
|
||||
public function getResultCount() {
|
||||
$search = $this->getCurrentSearch();
|
||||
// Each search is an array with the query as the first element and the results
|
||||
// array as the second.
|
||||
if (isset($search[1])) {
|
||||
return $search[1]['result count'];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for backend specific overrides to the settings form.
|
||||
*/
|
||||
public function settingsForm(&$form, &$form_state) {
|
||||
$facet = $form['#facetapi']['facet'];
|
||||
$realm = $form['#facetapi']['realm'];
|
||||
$facet_settings = $this->getFacet($facet)->getSettings();
|
||||
$options = $facet_settings->settings;
|
||||
$search_ids = variable_get('search_api_facets_search_ids', array());
|
||||
$search_ids = isset($search_ids[$this->info['instance']]) ? $search_ids[$this->info['instance']] : array();
|
||||
if (count($search_ids) > 1) {
|
||||
$form['global']['default_true'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Display for searches'),
|
||||
'#options' => array(
|
||||
TRUE => t('For all except the selected'),
|
||||
FALSE => t('Only for the selected'),
|
||||
),
|
||||
'#default_value' => isset($options['default_true']) ? $options['default_true'] : TRUE,
|
||||
);
|
||||
$form['global']['facet_search_ids'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Search IDs'),
|
||||
'#options' => $search_ids,
|
||||
'#size' => min(4, count($search_ids)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['global']['default_true'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => TRUE,
|
||||
);
|
||||
$form['global']['facet_search_ids'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
196
contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
Normal file
196
contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Date query type plugin for the Search API adapter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin for "date" query types.
|
||||
*/
|
||||
class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQueryTypeInterface {
|
||||
|
||||
/**
|
||||
* Loads the include file containing the date API functions.
|
||||
*/
|
||||
public function __construct(FacetapiAdapter $adapter, array $facet) {
|
||||
module_load_include('date.inc', 'facetapi');
|
||||
parent::__construct($adapter, $facet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query type associated with the plugin.
|
||||
*
|
||||
* @return string
|
||||
* The query type.
|
||||
*/
|
||||
static public function getType() {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the filter to the query object.
|
||||
*
|
||||
* @param $query
|
||||
* An object containing the query in the backend's native API.
|
||||
*/
|
||||
public function execute($query) {
|
||||
// Return terms for this facet.
|
||||
$this->adapter->addFacet($this->facet, $query);
|
||||
// Change limit to "unlimited" (-1).
|
||||
$options = &$query->getOptions();
|
||||
if (!empty($options['search_api_facets'][$this->facet['name']])) {
|
||||
$options['search_api_facets'][$this->facet['name']]['limit'] = -1;
|
||||
}
|
||||
|
||||
if ($active = $this->adapter->getActiveItems($this->facet)) {
|
||||
$item = end($active);
|
||||
$field = $this->facet['field'];
|
||||
$regex = str_replace(array('^', '$'), '', FACETAPI_REGEX_DATE);
|
||||
$filter = preg_replace_callback($regex, array($this, 'replaceDateString'), $item['value']);
|
||||
$this->addFacetFilter($query, $field, $filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replacement callback for replacing ISO dates with timestamps.
|
||||
*/
|
||||
public function replaceDateString($matches) {
|
||||
return strtotime($matches[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the facet's build array.
|
||||
*
|
||||
* @return array
|
||||
* The initialized render array.
|
||||
*/
|
||||
public function build() {
|
||||
$facet = $this->adapter->getFacet($this->facet);
|
||||
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
|
||||
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
|
||||
return array();
|
||||
}
|
||||
$search_id = $search_ids[$facet['name']];
|
||||
$build = array();
|
||||
$search = search_api_current_search($search_id);
|
||||
$results = $search[1];
|
||||
if (!$results['result count']) {
|
||||
return array();
|
||||
}
|
||||
// Gets total number of documents matched in search.
|
||||
$total = $results['result count'];
|
||||
|
||||
// Most of the code below is copied from search_facetapi's implementation of
|
||||
// this method.
|
||||
|
||||
// Executes query, iterates over results.
|
||||
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['field']])) {
|
||||
$values = $results['search_api_facets'][$this->facet['field']];
|
||||
foreach ($values as $value) {
|
||||
if ($value['count']) {
|
||||
$filter = $value['filter'];
|
||||
// We only process single values further. The "missing" filter and
|
||||
// range filters will be passed on unchanged.
|
||||
if ($filter == '!') {
|
||||
$build[$filter]['#count'] = $value['count'];
|
||||
}
|
||||
elseif ($filter[0] == '"') {
|
||||
$filter = substr($value['filter'], 1, -1);
|
||||
if ($filter) {
|
||||
$raw_values[$filter] = $value['count'];
|
||||
}
|
||||
}
|
||||
else {
|
||||
$filter = substr($value['filter'], 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$lower = facetapi_isodate(substr($filter, 0, $pos), FACETAPI_DATE_DAY);
|
||||
$upper = facetapi_isodate(substr($filter, $pos + 1), FACETAPI_DATE_DAY);
|
||||
$filter = '[' . $lower . ' TO ' . $upper . ']';
|
||||
}
|
||||
$build[$filter]['#count'] = $value['count'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets active facets, starts building hierarchy.
|
||||
$parent = $gap = NULL;
|
||||
foreach ($this->adapter->getActiveItems($this->facet) as $value => $item) {
|
||||
// If the item is active, the count is the result set count.
|
||||
$build[$value] = array('#count' => $total);
|
||||
|
||||
// Gets next "gap" increment, minute being the lowest we can go.
|
||||
if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) {
|
||||
continue;
|
||||
}
|
||||
$start = substr($value, 1, $pos);
|
||||
$end = substr($value, $pos + 4, -1);
|
||||
$date_gap = facetapi_get_date_gap($start, $end);
|
||||
$gap = facetapi_get_next_date_gap($date_gap, FACETAPI_DATE_MINUTE);
|
||||
|
||||
// If there is a previous item, there is a parent, uses a reference so the
|
||||
// arrays are populated when they are updated.
|
||||
if (NULL !== $parent) {
|
||||
$build[$parent]['#item_children'][$value] = &$build[$value];
|
||||
$build[$value]['#item_parents'][$parent] = $parent;
|
||||
}
|
||||
|
||||
// Stores the last value iterated over.
|
||||
$parent = $value;
|
||||
}
|
||||
if (empty($raw_values)) {
|
||||
return $build;
|
||||
}
|
||||
ksort($raw_values);
|
||||
|
||||
// Mind the gap! Calculates gap from min and max timestamps.
|
||||
$timestamps = array_keys($raw_values);
|
||||
if (NULL === $parent) {
|
||||
if (count($raw_values) > 1) {
|
||||
$gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps));
|
||||
}
|
||||
else {
|
||||
$gap = FACETAPI_DATE_HOUR;
|
||||
}
|
||||
}
|
||||
|
||||
// Converts all timestamps to dates in ISO 8601 format.
|
||||
$dates = array();
|
||||
foreach ($timestamps as $timestamp) {
|
||||
$dates[$timestamp] = facetapi_isodate($timestamp, $gap);
|
||||
}
|
||||
|
||||
// Treat each date as the range start and next date as the range end.
|
||||
$range_end = array();
|
||||
$previous = NULL;
|
||||
foreach (array_unique($dates) as $date) {
|
||||
if (NULL !== $previous) {
|
||||
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
|
||||
}
|
||||
$previous = $date;
|
||||
}
|
||||
$range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
|
||||
|
||||
// Groups dates by the range they belong to, builds the $build array
|
||||
// with the facet counts and formatted range values.
|
||||
foreach ($raw_values as $value => $count) {
|
||||
$new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']';
|
||||
if (!isset($build[$new_value])) {
|
||||
$build[$new_value] = array('#count' => $count);
|
||||
}
|
||||
else {
|
||||
$build[$new_value]['#count'] += $count;
|
||||
}
|
||||
|
||||
// Adds parent information if not already set.
|
||||
if (NULL !== $parent && $parent != $new_value) {
|
||||
$build[$parent]['#item_children'][$new_value] = &$build[$new_value];
|
||||
$build[$new_value]['#item_parents'][$parent] = $parent;
|
||||
}
|
||||
}
|
||||
|
||||
return $build;
|
||||
}
|
||||
}
|
149
contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
Normal file
149
contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Term query type plugin for the Apache Solr adapter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin for "term" query types.
|
||||
*/
|
||||
class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTypeInterface {
|
||||
|
||||
/**
|
||||
* Returns the query type associated with the plugin.
|
||||
*
|
||||
* @return string
|
||||
* The query type.
|
||||
*/
|
||||
static public function getType() {
|
||||
return 'term';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the filter to the query object.
|
||||
*
|
||||
* @param SearchApiQueryInterface $query
|
||||
* An object containing the query in the backend's native API.
|
||||
*/
|
||||
public function execute($query) {
|
||||
// Return terms for this facet.
|
||||
$this->adapter->addFacet($this->facet, $query);
|
||||
|
||||
$settings = $this->adapter->getFacet($this->facet)->getSettings();
|
||||
// Adds the operator parameter.
|
||||
$operator = $settings->settings['operator'];
|
||||
|
||||
// Add active facet filters.
|
||||
$active = $this->adapter->getActiveItems($this->facet);
|
||||
if (empty($active)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FACETAPI_OPERATOR_OR == $operator) {
|
||||
// If we're dealing with an OR facet, we need to use a nested filter.
|
||||
$facet_filter = $query->createFilter('OR');
|
||||
}
|
||||
else {
|
||||
// Otherwise we set the conditions directly on the query.
|
||||
$facet_filter = $query;
|
||||
}
|
||||
|
||||
foreach ($active as $filter => $filter_array) {
|
||||
$field = $this->facet['field'];
|
||||
$this->addFacetFilter($facet_filter, $field, $filter);
|
||||
}
|
||||
|
||||
// For OR facets, we now have to add the filter to the query.
|
||||
if (FACETAPI_OPERATOR_OR == $operator) {
|
||||
$query->filter($facet_filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for setting a facet filter on a query or query filter object.
|
||||
*/
|
||||
protected function addFacetFilter($query_filter, $field, $filter) {
|
||||
// Integer (or other nun-string) filters might mess up some of the following
|
||||
// comparison expressions.
|
||||
$filter = (string) $filter;
|
||||
if ($filter == '!') {
|
||||
$query_filter->condition($field, NULL);
|
||||
}
|
||||
elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
|
||||
$lower = trim(substr($filter, 1, $pos));
|
||||
$upper = trim(substr($filter, $pos + 4, -1));
|
||||
if ($lower == '*' && $upper == '*') {
|
||||
$query_filter->condition($field, NULL, '<>');
|
||||
}
|
||||
else {
|
||||
if ($lower != '*') {
|
||||
// Iff we have a range with two finite boundaries, we set two
|
||||
// conditions (larger than the lower bound and less than the upper
|
||||
// bound) and therefore have to make sure that we have an AND
|
||||
// conjunction for those.
|
||||
if ($upper != '*' && !($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
|
||||
$original_query_filter = $query_filter;
|
||||
$query_filter = new SearchApiQueryFilter('AND');
|
||||
}
|
||||
$query_filter->condition($field, $lower, '>=');
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$query_filter->condition($field, $upper, '<=');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$query_filter->condition($field, $filter);
|
||||
}
|
||||
if (isset($original_query_filter)) {
|
||||
$original_query_filter->filter($query_filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the facet's build array.
|
||||
*
|
||||
* @return array
|
||||
* The initialized render array.
|
||||
*/
|
||||
public function build() {
|
||||
$facet = $this->adapter->getFacet($this->facet);
|
||||
// The current search per facet is stored in a static variable (during
|
||||
// initActiveFilters) so that we can retrieve it here and get the correct
|
||||
// current search for this facet.
|
||||
$search_ids = drupal_static('search_api_facetapi_active_facets', array());
|
||||
if (empty($search_ids[$facet['name']]) || !search_api_current_search($search_ids[$facet['name']])) {
|
||||
return array();
|
||||
}
|
||||
$search_id = $search_ids[$facet['name']];
|
||||
$search = search_api_current_search($search_id);
|
||||
$build = array();
|
||||
$results = $search[1];
|
||||
if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['field']])) {
|
||||
$values = $results['search_api_facets'][$this->facet['field']];
|
||||
foreach ($values as $value) {
|
||||
$filter = $value['filter'];
|
||||
// As Facet API isn't really suited for our native facet filter
|
||||
// representations, convert the format here. (The missing facet can
|
||||
// stay the same.)
|
||||
if ($filter[0] == '"') {
|
||||
$filter = substr($filter, 1, -1);
|
||||
}
|
||||
elseif ($filter != '!') {
|
||||
// This is a range filter.
|
||||
$filter = substr($filter, 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
|
||||
}
|
||||
}
|
||||
$build[$filter] = array(
|
||||
'#count' => $value['count'],
|
||||
);
|
||||
}
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
31
contrib/search_api_facetapi/search_api_facetapi.api.php
Normal file
31
contrib/search_api_facetapi/search_api_facetapi.api.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Search facets module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets modules alter the search keys that are returned to FacetAPI and used
|
||||
* in the current search block and breadcrumb trail.
|
||||
*
|
||||
* @param string $keys
|
||||
* The string representing the user's current search query.
|
||||
* @param SearchApiQuery $query
|
||||
* The SearchApiQuery object for the current search.
|
||||
*/
|
||||
function hook_search_api_facetapi_keys_alter(&$keys, $query) {
|
||||
if ($keys == '[' . t('all items') . ']') {
|
||||
// Change $keys to something else, perhaps based on filters in the query
|
||||
// object.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
17
contrib/search_api_facetapi/search_api_facetapi.info
Normal file
17
contrib/search_api_facetapi/search_api_facetapi.info
Normal file
@ -0,0 +1,17 @@
|
||||
name = Search facets
|
||||
description = "Integrate the Search API with the Facet API to provide facetted searches."
|
||||
dependencies[] = search_api
|
||||
dependencies[] = facetapi
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
files[] = plugins/facetapi/adapter.inc
|
||||
files[] = plugins/facetapi/query_type_term.inc
|
||||
files[] = plugins/facetapi/query_type_date.inc
|
||||
|
||||
; Information added by drupal.org packaging script on 2013-01-09
|
||||
version = "7.x-1.4"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1357726719"
|
||||
|
13
contrib/search_api_facetapi/search_api_facetapi.install
Normal file
13
contrib/search_api_facetapi/search_api_facetapi.install
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the Search facets module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function search_api_facetapi_uninstall() {
|
||||
variable_del('search_api_facets_search_ids');
|
||||
}
|
381
contrib/search_api_facetapi/search_api_facetapi.module
Normal file
381
contrib/search_api_facetapi/search_api_facetapi.module
Normal file
@ -0,0 +1,381 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Integrates the Search API with the Facet API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function search_api_facetapi_menu() {
|
||||
// We need to handle our own menu paths for facets because we need a facet
|
||||
// configuration page per index.
|
||||
$first = TRUE;
|
||||
foreach (facetapi_get_realm_info() as $realm_name => $realm) {
|
||||
if ($first) {
|
||||
$first = FALSE;
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets'] = array(
|
||||
'title' => 'Facets',
|
||||
'page callback' => 'search_api_facetapi_settings',
|
||||
'page arguments' => array($realm_name, 5),
|
||||
'weight' => -1,
|
||||
'access arguments' => array('administer search_api'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
|
||||
);
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
|
||||
'title' => $realm['label'],
|
||||
'type' => MENU_DEFAULT_LOCAL_TASK,
|
||||
'weight' => $realm['weight'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
|
||||
'title' => $realm['label'],
|
||||
'page callback' => 'search_api_facetapi_settings',
|
||||
'page arguments' => array($realm_name, 5),
|
||||
'access arguments' => array('administer search_api'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
|
||||
'weight' => $realm['weight'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_searcher_info().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_searcher_info() {
|
||||
$info = array();
|
||||
$indexes = search_api_index_load_multiple(FALSE);
|
||||
foreach ($indexes as $index) {
|
||||
if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) {
|
||||
$searcher_name = 'search_api@' . $index->machine_name;
|
||||
$info[$searcher_name] = array(
|
||||
'label' => t('Search service: @name', array('@name' => $index->name)),
|
||||
'adapter' => 'search_api',
|
||||
'instance' => $index->machine_name,
|
||||
'types' => array($index->item_type),
|
||||
'path' => '',
|
||||
'supports facet missing' => TRUE,
|
||||
'supports facet mincount' => TRUE,
|
||||
'include default facets' => FALSE,
|
||||
);
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_facet_info().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
|
||||
$facet_info = array();
|
||||
if ('search_api' == $searcher_info['adapter']) {
|
||||
$index = search_api_index_load($searcher_info['instance']);
|
||||
if (!empty($index->options['fields'])) {
|
||||
$wrapper = $index->entityWrapper();
|
||||
$bundle_key = NULL;
|
||||
if (($entity_info = entity_get_info($index->item_type)) && !empty($entity_info['bundle keys']['bundle'])) {
|
||||
$bundle_key = $entity_info['bundle keys']['bundle'];
|
||||
}
|
||||
|
||||
// Some type-specific settings. Allowing to set some additional callbacks
|
||||
// (and other settings) in the map options allows for easier overriding by
|
||||
// other modules.
|
||||
$type_settings = array(
|
||||
'taxonomy_term' => array(
|
||||
'hierarchy callback' => 'facetapi_get_taxonomy_hierarchy',
|
||||
),
|
||||
'date' => array(
|
||||
'query type' => 'date',
|
||||
'map options' => array(
|
||||
'map callback' => 'facetapi_map_date',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Iterate through the indexed fields to set the facetapi settings for
|
||||
// each one.
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$field['key'] = $key;
|
||||
// Determine which, if any, of the field type-specific options will be
|
||||
// used for this field.
|
||||
$type = isset($field['entity_type']) ? $field['entity_type'] : $field['type'];
|
||||
$type_settings += array($type => array());
|
||||
|
||||
$facet_info[$key] = $type_settings[$type] + array(
|
||||
'label' => $field['name'],
|
||||
'description' => t('Filter by @type.', array('@type' => $field['name'])),
|
||||
'allowed operators' => array(
|
||||
FACETAPI_OPERATOR_AND => TRUE,
|
||||
FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'),
|
||||
),
|
||||
'dependency plugins' => array('role'),
|
||||
'facet missing allowed' => TRUE,
|
||||
'facet mincount allowed' => TRUE,
|
||||
'map callback' => 'search_api_facetapi_facet_map_callback',
|
||||
'map options' => array(),
|
||||
'field type' => $type,
|
||||
);
|
||||
if ($type === 'date') {
|
||||
$facet_info[$key]['description'] .= ' ' . t('(Caution: This may perform very poorly for large result sets.)');
|
||||
}
|
||||
$facet_info[$key]['map options'] += array(
|
||||
'field' => $field,
|
||||
'index id' => $index->machine_name,
|
||||
'value callback' => '_search_api_facetapi_facet_create_label',
|
||||
);
|
||||
// Find out whether this property is a Field API field.
|
||||
if (strpos($key, ':') === FALSE) {
|
||||
if (isset($wrapper->$key)) {
|
||||
$property_info = $wrapper->$key->info();
|
||||
if (!empty($property_info['field'])) {
|
||||
$facet_info[$key]['field api name'] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add bundle information, if applicable.
|
||||
if ($bundle_key) {
|
||||
if ($key === $bundle_key) {
|
||||
// Set entity type this field contains bundle information for.
|
||||
$facet_info[$key]['field api bundles'][] = $index->item_type;
|
||||
}
|
||||
else {
|
||||
// Add "bundle" as possible dependency plugin.
|
||||
$facet_info[$key]['dependency plugins'][] = 'bundle';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $facet_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_adapters().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_adapters() {
|
||||
return array(
|
||||
'search_api' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiAdapter',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_facetapi_query_types().
|
||||
*/
|
||||
function search_api_facetapi_facetapi_query_types() {
|
||||
return array(
|
||||
'search_api_term' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiTerm',
|
||||
'adapter' => 'search_api',
|
||||
),
|
||||
),
|
||||
'search_api_date' => array(
|
||||
'handler' => array(
|
||||
'class' => 'SearchApiFacetapiDate',
|
||||
'adapter' => 'search_api',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_query_alter().
|
||||
*
|
||||
* Adds Facet API support to the query.
|
||||
*/
|
||||
function search_api_facetapi_search_api_query_alter($query) {
|
||||
$index = $query->getIndex();
|
||||
if ($index->server()->supportsFeature('search_api_facets')) {
|
||||
// This is the main point of communication between the facet system and the
|
||||
// search back-end - it makes the query respond to active facets.
|
||||
$searcher = 'search_api@' . $index->machine_name;
|
||||
$adapter = facetapi_adapter_load($searcher);
|
||||
if ($adapter) {
|
||||
$adapter->addActiveFilters($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback for the facet settings page.
|
||||
*/
|
||||
function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
|
||||
if (!$index->enabled) {
|
||||
return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
|
||||
}
|
||||
if (!$index->server()->supportsFeature('search_api_facets')) {
|
||||
return array('#markup' => t('This index uses a server that does not support facet functionality.'));
|
||||
}
|
||||
$searcher_name = 'search_api@' . $index->machine_name;
|
||||
module_load_include('inc', 'facetapi', 'facetapi.admin');
|
||||
return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map callback for all search_api facet fields.
|
||||
*
|
||||
* @param array $values
|
||||
* The values to map.
|
||||
* @param array $options
|
||||
* An associative array containing:
|
||||
* - field: Field information, as stored in the index, but with an additional
|
||||
* "key" property set to the field's internal name.
|
||||
* - index id: The machine name of the index for this facet.
|
||||
* - map callback: (optional) A callback that will be called at the beginning,
|
||||
* which allows initial mapping of filters. Only values not mapped by that
|
||||
* callback will be processed by this method.
|
||||
* - value callback: A callback used to map single values and the limits of
|
||||
* ranges. The signature is the same as for this function, but all values
|
||||
* will be single values.
|
||||
* - missing label: (optional) The label used for the "missing" facet.
|
||||
*
|
||||
* @return array
|
||||
* An array mapping raw filter values to their labels.
|
||||
*/
|
||||
function search_api_facetapi_facet_map_callback(array $values, array $options = array()) {
|
||||
$map = array();
|
||||
// See if we have an additional map callback.
|
||||
if (isset($options['map callback']) && is_callable($options['map callback'])) {
|
||||
$map = call_user_func($options['map callback'], $values, $options);
|
||||
}
|
||||
|
||||
// Then look at all unmapped values and save information for them.
|
||||
$mappable_values = array();
|
||||
$ranges = array();
|
||||
foreach ($values as $value) {
|
||||
$value = (string) $value;
|
||||
if (isset($map[$value])) {
|
||||
continue;
|
||||
}
|
||||
if ($value == '!') {
|
||||
// The "missing" filter is usually always the same, but we allow an easy
|
||||
// override via the "missing label" map option.
|
||||
$map['!'] = isset($options['missing label']) ? $options['missing label'] : '(' . t('none') . ')';
|
||||
continue;
|
||||
}
|
||||
$length = strlen($value);
|
||||
if ($length > 5 && $value[0] == '[' && $value[$length - 1] == ']' && ($pos = strpos($value, ' TO '))) {
|
||||
// This is a range filter.
|
||||
$lower = trim(substr($value, 1, $pos));
|
||||
$upper = trim(substr($value, $pos + 4, -1));
|
||||
if ($lower != '*') {
|
||||
$mappable_values[$lower] = TRUE;
|
||||
}
|
||||
if ($upper != '*') {
|
||||
$mappable_values[$upper] = TRUE;
|
||||
}
|
||||
$ranges[$value] = array(
|
||||
'lower' => $lower,
|
||||
'upper' => $upper,
|
||||
);
|
||||
}
|
||||
else {
|
||||
// A normal, single-value filter.
|
||||
$mappable_values[$value] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mappable_values) {
|
||||
$map += call_user_func($options['value callback'], array_keys($mappable_values), $options);
|
||||
}
|
||||
|
||||
foreach ($ranges as $value => $range) {
|
||||
$lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
|
||||
$upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
|
||||
if ($lower == '*' && $upper == '*') {
|
||||
$map[$value] = t('any');
|
||||
}
|
||||
elseif ($lower == '*') {
|
||||
$map[$value] = "< $upper";
|
||||
}
|
||||
elseif ($upper == '*') {
|
||||
$map[$value] = "> $lower";
|
||||
}
|
||||
else {
|
||||
$map[$value] = "$lower – $upper";
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a human-readable label for single facet filter values.
|
||||
*/
|
||||
function _search_api_facetapi_facet_create_label(array $values, array $options) {
|
||||
$field = $options['field'];
|
||||
// For entities, we can simply use the entity labels.
|
||||
if (isset($field['entity_type'])) {
|
||||
$type = $field['entity_type'];
|
||||
$entities = entity_load($type, $values);
|
||||
$map = array();
|
||||
foreach ($entities as $id => $entity) {
|
||||
$label = entity_label($type, $entity);
|
||||
if ($label) {
|
||||
$map[$id] = $label;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
// Then, we check whether there is an options list for the field.
|
||||
$index = search_api_index_load($options['index id']);
|
||||
$wrapper = $index->entityWrapper();
|
||||
foreach (explode(':', $field['key']) as $part) {
|
||||
if (!isset($wrapper->$part)) {
|
||||
$wrapper = NULL;
|
||||
break;
|
||||
}
|
||||
$wrapper = $wrapper->$part;
|
||||
while (($info = $wrapper->info()) && search_api_is_list_type($info['type'])) {
|
||||
$wrapper = $wrapper[0];
|
||||
}
|
||||
}
|
||||
if ($wrapper && ($options = $wrapper->optionsList('view'))) {
|
||||
return $options;
|
||||
}
|
||||
// As a "last resort" we try to create a label based on the field type.
|
||||
$map = array();
|
||||
foreach ($values as $value) {
|
||||
switch ($field['type']) {
|
||||
case 'boolean':
|
||||
$map[$value] = $value ? t('true') : t('false');
|
||||
break;
|
||||
case 'date':
|
||||
$v = is_numeric($value) ? $value : strtotime($value);
|
||||
$map[$value] = format_date($v, 'short');
|
||||
break;
|
||||
case 'duration':
|
||||
$map[$value] = format_interval($value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function search_api_facetapi_form_search_api_admin_index_fields_alter(&$form, &$form_state) {
|
||||
$form['#submit'][] = 'search_api_facetapi_search_api_admin_index_fields_submit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for search_api_admin_index_fields().
|
||||
*/
|
||||
function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_state) {
|
||||
// Clears this searcher's cached facet definitions.
|
||||
$cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
|
||||
cache_clear_all($cid, 'cache', TRUE);
|
||||
}
|
71
contrib/search_api_views/README.txt
Normal file
71
contrib/search_api_views/README.txt
Normal file
@ -0,0 +1,71 @@
|
||||
Search API Views integration
|
||||
----------------------------
|
||||
|
||||
This module integrates the Search API with the popular Views module [1],
|
||||
allowing users to create views with filters, arguments, sorts and fields based
|
||||
on any search index.
|
||||
|
||||
[1] http://drupal.org/project/views
|
||||
|
||||
"More like this" feature
|
||||
------------------------
|
||||
This module defines the "More like this" feature (feature key: "search_api_mlt")
|
||||
that search service classes can implement. With a server supporting this, you
|
||||
can use the „More like this“ contextual filter to display a list of items
|
||||
related to a given item (usually, nodes similar to the node currently viewed).
|
||||
|
||||
For developers:
|
||||
A service class that wants to support this feature has to check for a
|
||||
"search_api_mlt" option in the search() method. When present, it will be an
|
||||
array containing two keys:
|
||||
- id: The entity ID of the item to which related items should be searched.
|
||||
- fields: An array of indexed fields to use for testing the similarity of items.
|
||||
When these are present, the normal keywords should be ignored and the related
|
||||
items be returned as results instead. Sorting, filtering and range restriction
|
||||
should all work normally.
|
||||
|
||||
"Facets block" display
|
||||
----------------------
|
||||
Most features should be clear to users of Views. However, the module also
|
||||
provides a new display type, "Facets block", that might need some explanation.
|
||||
This display type is only available, if the „Search facets“ module is also
|
||||
enabled.
|
||||
|
||||
The basic use of the block is to provide a list of links to the most popular
|
||||
filter terms (i.e., the ones with the most results) for a certain category. For
|
||||
example, you could provide a block listing the most popular authors, or taxonomy
|
||||
terms, linking to searches for those, to provide some kind of landing page.
|
||||
|
||||
Please note that, due to limitations in Views, this display mode is shown for
|
||||
views of all base tables, even though it only works for views based on Search
|
||||
API indexes. For views of other base tables, this will just print an error
|
||||
message.
|
||||
The display will also always ignore the view's "Style" setting, selected fields
|
||||
and sorts, etc.
|
||||
|
||||
To use the display, specify the base path of the search you want to link to
|
||||
(this enables you to also link to searches that aren't based on Views) and the
|
||||
facet field to use (any indexed field can be used here, there needn't be a facet
|
||||
defined for it). You'll then have the block available in the blocks
|
||||
administration and can enable and move it at leisure.
|
||||
Note, however, that the facet in question has to be enabled for the search page
|
||||
linked to for the filter to have an effect.
|
||||
|
||||
Since the block will trigger a search on pages where it is set to appear, you
|
||||
can also enable additional „normal“ facet blocks for that search, via the
|
||||
„Facets“ tab for the index. They will automatically also point to the same
|
||||
search that you specified for the display. The Search ID of the „Facets blocks“
|
||||
display can easily be recognized by the "-facet_block" suffix.
|
||||
If you want to use only the normal facets and not display anything at all in
|
||||
the Views block, just activate the display's „Hide block“ option.
|
||||
|
||||
Note: If you want to display the block not only on a few pages, you should in
|
||||
any case take care that it isn't displayed on the search page, since that might
|
||||
confuse users.
|
||||
|
||||
FAQ: Why „*Indexed* Node“?
|
||||
--------------------------
|
||||
The group name used for the search result itself (in fields, filters, etc.) is
|
||||
prefixed with „Indexed“ in order to be distinguishable from fields on referenced
|
||||
nodes (or other entities). The data displayed normally still comes from the
|
||||
entity, not from the search index.
|
262
contrib/search_api_views/includes/display_facet_block.inc
Normal file
262
contrib/search_api_views/includes/display_facet_block.inc
Normal file
@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Display plugin for displaying the search facets in a block.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Plugin class for displaying search facets in a block.
|
||||
*/
|
||||
class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
|
||||
public function displays_exposed() {
|
||||
return FALSE;
|
||||
}
|
||||
public function uses_exposed() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['linked_path'] = array('default' => '');
|
||||
$options['facet_field'] = '';
|
||||
$options['hide_block'] = FALSE;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($form_state['section']) {
|
||||
case 'linked_path':
|
||||
$form['#title'] .= t('Search page path');
|
||||
$form['linked_path'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#description' => t('The menu path to which search facets will link. Leave empty to use the current path.'),
|
||||
'#default_value' => $this->get_option('linked_path'),
|
||||
);
|
||||
break;
|
||||
case 'facet_field':
|
||||
$form['facet_field'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Facet field'),
|
||||
'#options' => $this->getFieldOptions(),
|
||||
'#default_value' => $this->get_option('facet_field'),
|
||||
);
|
||||
break;
|
||||
case 'use_more':
|
||||
$form['use_more']['#description'] = t('This will add a more link to the bottom of this view, which will link to the base path for the facet links.');
|
||||
$form['use_more_always'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $this->get_option('use_more_always'),
|
||||
);
|
||||
break;
|
||||
case 'hide_block':
|
||||
$form['hide_block'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Hide block'),
|
||||
'#description' => t('Hide this block, but still execute the search. ' .
|
||||
'Can be used to show native Facet API facet blocks linking to the search page specified above.'),
|
||||
'#default_value' => $this->get_option('hide_block'),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function options_validate(&$form, &$form_state) {
|
||||
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
|
||||
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
|
||||
}
|
||||
}
|
||||
|
||||
public function options_submit(&$form, &$form_state) {
|
||||
parent::options_submit($form, $form_state);
|
||||
|
||||
switch ($form_state['section']) {
|
||||
case 'linked_path':
|
||||
$this->set_option('linked_path', $form_state['values']['linked_path']);
|
||||
break;
|
||||
case 'facet_field':
|
||||
$this->set_option('facet_field', $form_state['values']['facet_field']);
|
||||
break;
|
||||
case 'hide_block':
|
||||
$this->set_option('hide_block', $form_state['values']['hide_block']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function options_summary(&$categories, &$options) {
|
||||
parent::options_summary($categories, $options);
|
||||
|
||||
$options['linked_path'] = array(
|
||||
'category' => 'block',
|
||||
'title' => t('Search page path'),
|
||||
'value' => $this->get_option('linked_path') ? $this->get_option('linked_path') : t('Use current path'),
|
||||
);
|
||||
$field_options = $this->getFieldOptions();
|
||||
$options['facet_field'] = array(
|
||||
'category' => 'block',
|
||||
'title' => t('Facet field'),
|
||||
'value' => $this->get_option('facet_field') ? $field_options[$this->get_option('facet_field')] : t('None'),
|
||||
);
|
||||
$options['hide_block'] = array(
|
||||
'category' => 'block',
|
||||
'title' => t('Hide block'),
|
||||
'value' => $this->get_option('hide_block') ? t('Yes') : t('No'),
|
||||
);
|
||||
}
|
||||
|
||||
protected $field_options = NULL;
|
||||
|
||||
protected function getFieldOptions() {
|
||||
if (!isset($this->field_options)) {
|
||||
$index_id = substr($this->view->base_table, 17);
|
||||
if (!($index_id && ($index = search_api_index_load($index_id)))) {
|
||||
$table = views_fetch_data($this->view->base_table);
|
||||
$table = empty($table['table']['base']['title']) ? $this->view->base_table : $table['table']['base']['title'];
|
||||
throw new SearchApiException(t('The "Facets block" display cannot be used with a view for @basetable. ' .
|
||||
'Please only use this display with base tables representing search indexes.',
|
||||
array('@basetable' => $table)));
|
||||
}
|
||||
$this->field_options = array();
|
||||
if (!empty($index->options['fields'])) {
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$this->field_options[$key] = $field['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->field_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the 'more' link
|
||||
*/
|
||||
public function render_more_link() {
|
||||
if ($this->use_more()) {
|
||||
$path = $this->get_option('linked_path');
|
||||
$theme = views_theme_functions('views_more', $this->view, $this->display);
|
||||
$path = check_url(url($path, array()));
|
||||
|
||||
return array(
|
||||
'#theme' => $theme,
|
||||
'#more_url' => $path,
|
||||
'#link_text' => check_plain($this->use_more_text()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute() {
|
||||
if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
|
||||
form_set_error('', t('The "Facets block" display can only be used with base tables based on Search API indexes.'));
|
||||
return NULL;
|
||||
}
|
||||
$facet_field = $this->get_option('facet_field');
|
||||
if (!$facet_field) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$base_path = $this->get_option('linked_path');
|
||||
if (!$base_path) {
|
||||
$base_path = $_GET['q'];
|
||||
}
|
||||
$this->view->build();
|
||||
$limit = empty($this->view->query->pager->options['items_per_page']) ? 10 : $this->view->query->pager->options['items_per_page'];
|
||||
$query_options = &$this->view->query->getOptions();
|
||||
if (!$this->get_option('hide_block')) {
|
||||
// If we hide the block, we don't need this extra facet.
|
||||
$query_options['search_api_facets']['search_api_views_facets_block'] = array(
|
||||
'field' => $facet_field,
|
||||
'limit' => $limit,
|
||||
'missing' => FALSE,
|
||||
'min_count' => 1,
|
||||
);
|
||||
}
|
||||
$query_options['search id'] = 'search_api_views:' . $this->view->name . '-facets_block';
|
||||
$query_options['search_api_base_path'] = $base_path;
|
||||
$this->view->query->range(0, 0);
|
||||
|
||||
$this->view->execute();
|
||||
|
||||
if ($this->get_option('hide_block')) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$results = $this->view->query->getSearchApiResults();
|
||||
|
||||
if (empty($results['search_api_facets']['search_api_views_facets_block'])) {
|
||||
return NULL;
|
||||
}
|
||||
$terms = $results['search_api_facets']['search_api_views_facets_block'];
|
||||
|
||||
$filters = array();
|
||||
foreach ($terms as $term) {
|
||||
$filter = $term['filter'];
|
||||
if ($filter[0] == '"') {
|
||||
$filter = substr($filter, 1, -1);
|
||||
}
|
||||
elseif ($filter != '!') {
|
||||
// This is a range filter.
|
||||
$filter = substr($filter, 1, -1);
|
||||
$pos = strpos($filter, ' ');
|
||||
if ($pos !== FALSE) {
|
||||
$filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
|
||||
}
|
||||
}
|
||||
$filters[$term['filter']] = $filter;
|
||||
}
|
||||
|
||||
$index = $this->view->query->getIndex();
|
||||
$options['field'] = $index->options['fields'][$facet_field];
|
||||
$options['field']['key'] = $facet_field;
|
||||
$options['index id'] = $index->machine_name;
|
||||
$options['value callback'] = '_search_api_facetapi_facet_create_label';
|
||||
$map = search_api_facetapi_facet_map_callback($filters, $options);
|
||||
|
||||
$facets = array();
|
||||
$prefix = rawurlencode($facet_field) . ':';
|
||||
foreach ($terms as $term) {
|
||||
$name = $filter = $filters[$term['filter']];
|
||||
if (isset($map[$filter])) {
|
||||
$name = $map[$filter];
|
||||
}
|
||||
$query['f'][0] = $prefix . $filter;
|
||||
|
||||
// Initializes variables passed to theme hook.
|
||||
$variables = array(
|
||||
'text' => $name,
|
||||
'path' => $base_path,
|
||||
'count' => $term['count'],
|
||||
'options' => array(
|
||||
'attributes' => array('class' => 'facetapi-inactive'),
|
||||
'html' => FALSE,
|
||||
'query' => $query,
|
||||
),
|
||||
);
|
||||
|
||||
// Themes the link, adds row to facets.
|
||||
$facets[] = array(
|
||||
'class' => array('leaf'),
|
||||
'data' => theme('facetapi_link_inactive', $variables),
|
||||
);
|
||||
}
|
||||
|
||||
if (!$facets) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$info['content']['facets'] = array(
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $facets,
|
||||
);
|
||||
$info['content']['more'] = $this->render_more_link();
|
||||
$info['subject'] = filter_xss_admin($this->view->get_title());
|
||||
return $info;
|
||||
}
|
||||
|
||||
}
|
122
contrib/search_api_views/includes/handler_argument.inc
Normal file
122
contrib/search_api_views/includes/handler_argument.inc
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling all non-fulltext types.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgument extends views_handler_argument {
|
||||
|
||||
/**
|
||||
* The associated views query object.
|
||||
*
|
||||
* @var SearchApiViewsQuery
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Determine if the argument can generate a breadcrumb
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
// @todo Change and implement set_breadcrumb()?
|
||||
public function uses_breadcrumb() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a list of default behaviors for this argument if the argument
|
||||
* is not present.
|
||||
*
|
||||
* Override this method to provide additional (or fewer) default behaviors.
|
||||
*/
|
||||
public function default_actions($which = NULL) {
|
||||
$defaults = array(
|
||||
'ignore' => array(
|
||||
'title' => t('Display all values'),
|
||||
'method' => 'default_ignore',
|
||||
'breadcrumb' => TRUE, // generate a breadcrumb to here
|
||||
),
|
||||
'not found' => array(
|
||||
'title' => t('Hide view / Page not found (404)'),
|
||||
'method' => 'default_not_found',
|
||||
'hard fail' => TRUE, // This is a hard fail condition
|
||||
),
|
||||
'empty' => array(
|
||||
'title' => t('Display empty text'),
|
||||
'method' => 'default_empty',
|
||||
'breadcrumb' => TRUE, // generate a breadcrumb to here
|
||||
),
|
||||
'default' => array(
|
||||
'title' => t('Provide default argument'),
|
||||
'method' => 'default_default',
|
||||
'form method' => 'default_argument_form',
|
||||
'has default argument' => TRUE,
|
||||
'default only' => TRUE, // this can only be used for missing argument, not validation failure
|
||||
),
|
||||
);
|
||||
|
||||
if ($which) {
|
||||
return isset($defaults[$which]) ? $defaults[$which] : NULL;
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['break_phrase'] = array('default' => FALSE);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
// Allow passing multiple values.
|
||||
$form['break_phrase'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Allow multiple values'),
|
||||
'#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'),
|
||||
'#default_value' => $this->options['break_phrase'],
|
||||
'#fieldset' => 'more',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
// @todo Provide options to select the operator, instead of always using '='?
|
||||
public function query($group_by = FALSE) {
|
||||
if (!empty($this->options['break_phrase'])) {
|
||||
views_break_phrase($this->argument, $this);
|
||||
}
|
||||
else {
|
||||
$this->value = array($this->argument);
|
||||
}
|
||||
|
||||
if (count($this->value) > 1) {
|
||||
$filter = $this->query->createFilter(drupal_strtoupper($this->operator));
|
||||
// $filter will be NULL if there were errors in the query.
|
||||
if ($filter) {
|
||||
foreach ($this->value as $value) {
|
||||
$filter->condition($this->real_field, $value, '=');
|
||||
}
|
||||
$this->query->filter($filter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->query->condition($this->real_field, reset($this->value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title this argument will assign the view, given the argument.
|
||||
*
|
||||
* This usually needs to be overridden to provide a proper title.
|
||||
*/
|
||||
public function title() {
|
||||
return t('Search @field for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgumentText {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
$options['fields'] = array('default' => array());
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$fields = $this->getFulltextFields();
|
||||
if (!empty($fields)) {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Searched fields'),
|
||||
'#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
|
||||
'#options' => $fields,
|
||||
'#size' => min(4, count($fields)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
if ($this->options['fields']) {
|
||||
$this->query->fields($this->options['fields']);
|
||||
}
|
||||
|
||||
$old = $this->query->getOriginalKeys();
|
||||
$this->query->keys($this->argument);
|
||||
if ($old) {
|
||||
$keys = &$this->query->getKeys();
|
||||
if (is_array($keys)) {
|
||||
$keys[] = $old;
|
||||
}
|
||||
elseif (is_array($old)) {
|
||||
// We don't support such nonsense.
|
||||
}
|
||||
else {
|
||||
$keys = "($old) ($keys)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get an option list of all available fulltext fields.
|
||||
*/
|
||||
protected function getFulltextFields() {
|
||||
$ret = array();
|
||||
$index = search_api_index_load(substr($this->table, 17));
|
||||
if (!empty($index->options['fields'])) {
|
||||
$fields = $index->getFields();
|
||||
foreach ($index->getFulltextFields() as $field) {
|
||||
$ret[$field] = $fields[$field]['name'];
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler providing a list of related items for search servers
|
||||
* supporting the "search_api_mlt" feature.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
unset($options['break_phrase']);
|
||||
$options['fields'] = array('default' => array());
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
unset($form['break_phrase']);
|
||||
|
||||
$index = search_api_index_load(substr($this->table, 17));
|
||||
if (!empty($index->options['fields'])) {
|
||||
$fields = array();
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$fields[$key] = $field['name'];
|
||||
}
|
||||
}
|
||||
if (!empty($fields)) {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Fields for Similarity'),
|
||||
'#description' => t('Select the fields that will be used for finding similar content. If no fields are selected, all available fields will be used.'),
|
||||
'#options' => $fields,
|
||||
'#size' => min(8, count($fields)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the query for this argument.
|
||||
*
|
||||
* The argument sent may be found at $this->argument.
|
||||
*/
|
||||
public function query($group_by = FALSE) {
|
||||
$server = $this->query->getIndex()->server();
|
||||
if (!$server->supportsFeature('search_api_mlt')) {
|
||||
$class = search_api_get_service_info($server->class);
|
||||
throw new SearchApiException(t('The search service "@class" does not offer "More like this" functionality.',
|
||||
array('@class' => $class['name'])));
|
||||
return;
|
||||
}
|
||||
$fields = $this->options['fields'] ? $this->options['fields'] : array();
|
||||
if (empty($fields)) {
|
||||
foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
|
||||
$fields[] = $key;
|
||||
}
|
||||
}
|
||||
$mlt = array(
|
||||
'id' => $this->argument,
|
||||
'fields' => $fields,
|
||||
);
|
||||
$this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
|
||||
}
|
||||
}
|
17
contrib/search_api_views/includes/handler_argument_text.inc
Normal file
17
contrib/search_api_views/includes/handler_argument_text.inc
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views argument handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerArgumentText extends SearchApiViewsHandlerArgument {
|
||||
|
||||
/**
|
||||
* Get the title this argument will assign the view, given the argument.
|
||||
*
|
||||
* This usually needs to be overridden to provide a proper title.
|
||||
*/
|
||||
public function title() {
|
||||
return t('Search for "@arg"', array('@field' => $this->definition['title'], '@arg' => $this->argument));
|
||||
}
|
||||
|
||||
}
|
105
contrib/search_api_views/includes/handler_filter.inc
Normal file
105
contrib/search_api_views/includes/handler_filter.inc
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler base class for handling all "normal" cases.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilter extends views_handler_filter {
|
||||
|
||||
/**
|
||||
* The value to filter for.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* The operator used for filtering.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $operator;
|
||||
|
||||
/**
|
||||
* The associated views query object.
|
||||
*
|
||||
* @var SearchApiViewsQuery
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array(
|
||||
'<' => t('Is smaller than'),
|
||||
'<=' => t('Is smaller than or equal to'),
|
||||
'=' => t('Is equal to'),
|
||||
'<>' => t('Is not equal to'),
|
||||
'>=' => t('Is greater than or equal to'),
|
||||
'>' => t('Is greater than'),
|
||||
'empty' => t('Is empty'),
|
||||
'not empty' => t('Is not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting the filter value.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? array_shift($this->value) : NULL;
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#size' => 30,
|
||||
'#default_value' => isset($this->value) ? $this->value : '',
|
||||
);
|
||||
|
||||
// Hide the value box if the operator is 'empty' or 'not empty'.
|
||||
// Radios share the same selector so we have to add some dummy selector.
|
||||
$form['value']['#states']['visible'] = array(
|
||||
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
|
||||
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the filter on the administrative summary
|
||||
*/
|
||||
function admin_summary() {
|
||||
if (!empty($this->options['exposed'])) {
|
||||
return t('exposed');
|
||||
}
|
||||
|
||||
if ($this->operator === 'empty') {
|
||||
return t('is empty');
|
||||
}
|
||||
if ($this->operator === 'not empty') {
|
||||
return t('is not empty');
|
||||
}
|
||||
|
||||
return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
if ($this->operator === 'empty') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
elseif ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
}
|
||||
else {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? reset($this->value) : NULL;
|
||||
}
|
||||
if (strlen($this->value) > 0) {
|
||||
$this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
30
contrib/search_api_views/includes/handler_filter_boolean.inc
Normal file
30
contrib/search_api_views/includes/handler_filter_boolean.inc
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterBoolean extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting the filter value.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? array_shift($this->value) : NULL;
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#options' => array(1 => t('True'), 0 => t('False')),
|
||||
'#default_value' => isset($this->value) ? $this->value : '',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
86
contrib/search_api_views/includes/handler_filter_date.inc
Normal file
86
contrib/search_api_views/includes/handler_filter_date.inc
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler base class for handling all "normal" cases.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Add a "widget type" option.
|
||||
*/
|
||||
public function option_definition() {
|
||||
return parent::option_definition() + array(
|
||||
'widget_type' => array('default' => 'default'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the date popup module is enabled, provide the extra option setting.
|
||||
*/
|
||||
public function has_extra_options() {
|
||||
if (module_exists('date_popup')) {
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add extra options if we allow the date popup widget.
|
||||
*/
|
||||
public function extra_options_form(&$form, &$form_state) {
|
||||
parent::extra_options_form($form, $form_state);
|
||||
if (module_exists('date_popup')) {
|
||||
$widget_options = array('default' => 'Default', 'date_popup' => 'Date popup');
|
||||
$form['widget_type'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Date selection form element'),
|
||||
'#default_value' => $this->options['widget_type'],
|
||||
'#options' => $widget_options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting the filter value.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
|
||||
// If we are using the date popup widget, overwrite the settings of the form
|
||||
// according to what date_popup expects.
|
||||
if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) {
|
||||
$form['value']['#type'] = 'date_popup';
|
||||
$form['value']['#date_format'] = 'm/d/Y';
|
||||
unset($form['value']['#description']);
|
||||
}
|
||||
elseif (empty($form_state['exposed'])) {
|
||||
$form['value']['#description'] = t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "@date1" or "@date2".', array(
|
||||
'@doc-link' => 'http://php.net/manual/en/function.strtotime.php',
|
||||
'@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'),
|
||||
'@date2' => 'now + 1 day',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
if ($this->operator === 'empty') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
elseif ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
}
|
||||
else {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? reset($this->value) : NULL;
|
||||
}
|
||||
$v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
|
||||
if ($v !== FALSE) {
|
||||
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
131
contrib/search_api_views/includes/handler_filter_fulltext.inc
Normal file
131
contrib/search_api_views/includes/handler_filter_fulltext.inc
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterText {
|
||||
|
||||
/**
|
||||
* Specify the options this filter uses.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
|
||||
$options['mode'] = array('default' => 'keys');
|
||||
$options['fields'] = array('default' => array());
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the options form a bit.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$form['mode'] = array(
|
||||
'#title' => t('Use as'),
|
||||
'#type' => 'radios',
|
||||
'#options' => array(
|
||||
'keys' => t('Search keys – multiple words will be split and the filter will influence relevance.'),
|
||||
'filter' => t("Search filter – use as a single phrase that restricts the result set but doesn't influence relevance."),
|
||||
),
|
||||
'#default_value' => $this->options['mode'],
|
||||
);
|
||||
|
||||
$fields = $this->getFulltextFields();
|
||||
if (!empty($fields)) {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Searched fields'),
|
||||
'#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
|
||||
'#options' => $fields,
|
||||
'#size' => min(4, count($fields)),
|
||||
'#multiple' => TRUE,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['fields'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => array(),
|
||||
);
|
||||
}
|
||||
if (isset($form['expose'])) {
|
||||
$form['expose']['#weight'] = -5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
while (is_array($this->value)) {
|
||||
$this->value = $this->value ? reset($this->value) : '';
|
||||
}
|
||||
// Catch empty strings entered by the user, but not "0".
|
||||
if ($this->value === '') {
|
||||
return;
|
||||
}
|
||||
$fields = $this->options['fields'];
|
||||
$fields = $fields ? $fields : array_keys($this->getFulltextFields());
|
||||
|
||||
// If something already specifically set different fields, we silently fall
|
||||
// back to mere filtering.
|
||||
$filter = $this->options['mode'] == 'filter';
|
||||
if (!$filter) {
|
||||
$old = $this->query->getFields();
|
||||
$filter = $old && (array_diff($old, $fields) || array_diff($fields, $old));
|
||||
}
|
||||
|
||||
if ($filter) {
|
||||
$filter = $this->query->createFilter('OR');
|
||||
foreach ($fields as $field) {
|
||||
$filter->condition($field, $this->value, $this->operator);
|
||||
}
|
||||
$this->query->filter($filter);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->query->fields($fields);
|
||||
$old = $this->query->getOriginalKeys();
|
||||
$this->query->keys($this->value);
|
||||
if ($this->operator != '=') {
|
||||
$keys = &$this->query->getKeys();
|
||||
if (is_array($keys)) {
|
||||
$keys['#negation'] = TRUE;
|
||||
}
|
||||
else {
|
||||
// We can't know how negation is expressed in the server's syntax.
|
||||
}
|
||||
}
|
||||
if ($old) {
|
||||
$keys = &$this->query->getKeys();
|
||||
if (is_array($keys)) {
|
||||
$keys[] = $old;
|
||||
}
|
||||
elseif (is_array($old)) {
|
||||
// We don't support such nonsense.
|
||||
}
|
||||
else {
|
||||
$keys = "($old) ($keys)";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get an option list of all available fulltext fields.
|
||||
*/
|
||||
protected function getFulltextFields() {
|
||||
$fields = array();
|
||||
$index = search_api_index_load(substr($this->table, 17));
|
||||
if (!empty($index->options['fields'])) {
|
||||
$f = $index->getFields();
|
||||
foreach ($index->getFulltextFields() as $name) {
|
||||
$fields[$name] = $f[$name]['name'];
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiViewsHandlerFilterLanguage class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling the special "Item language" field.
|
||||
*
|
||||
* Definition items:
|
||||
* - options: An array of possible values for this field.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions {
|
||||
|
||||
/**
|
||||
* Provide a form for setting options.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
parent::value_form($form, $form_state);
|
||||
$form['value']['#options'] = array(
|
||||
'current' => t("Current user's language"),
|
||||
'default' => t('Default site language'),
|
||||
) + $form['value']['#options'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a summary of this filter's value for the admin UI.
|
||||
*/
|
||||
public function admin_summary() {
|
||||
$tmp = $this->definition['options'];
|
||||
$this->definition['options']['current'] = t('current');
|
||||
$this->definition['options']['default'] = t('default');
|
||||
$ret = parent::admin_summary();
|
||||
$this->definition['options'] = $tmp;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
global $language_content;
|
||||
foreach ($this->value as $i => $v) {
|
||||
if ($v == 'current') {
|
||||
$this->value[$i] = $language_content->language;
|
||||
}
|
||||
elseif ($v == 'default') {
|
||||
$this->value[$i] = language_default('language');
|
||||
}
|
||||
}
|
||||
parent::query();
|
||||
}
|
||||
|
||||
}
|
205
contrib/search_api_views/includes/handler_filter_options.inc
Normal file
205
contrib/search_api_views/includes/handler_filter_options.inc
Normal file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fields with a limited set of possible
|
||||
* values.
|
||||
*
|
||||
* Definition items:
|
||||
* - options: An array of possible values for this field.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
|
||||
|
||||
protected $value_form_type = 'checkboxes';
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array(
|
||||
'=' => t('Is one of'),
|
||||
'<>' => t('Is not one of'),
|
||||
'empty' => t('Is empty'),
|
||||
'not empty' => t('Is not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set "reduce" option to FALSE by default.
|
||||
*/
|
||||
public function expose_options() {
|
||||
parent::expose_options();
|
||||
$this->options['expose']['reduce'] = FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the "reduce" option to the exposed form.
|
||||
*/
|
||||
public function expose_form(&$form, &$form_state) {
|
||||
parent::expose_form($form, $form_state);
|
||||
$form['expose']['reduce'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Limit list to selected items'),
|
||||
'#description' => t('If checked, the only items presented to the user will be the ones selected here.'),
|
||||
'#default_value' => !empty($this->options['expose']['reduce']),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define "reduce" option.
|
||||
*/
|
||||
public function option_definition() {
|
||||
$options = parent::option_definition();
|
||||
$options['expose']['contains']['reduce'] = array('default' => FALSE);
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the options according to the selection.
|
||||
*/
|
||||
protected function reduce_value_options() {
|
||||
$options = array();
|
||||
foreach ($this->definition['options'] as $id => $option) {
|
||||
if (isset($this->options['value'][$id])) {
|
||||
$options[$id] = $option;
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save set checkboxes.
|
||||
*/
|
||||
public function value_submit($form, &$form_state) {
|
||||
// Drupal's FAPI system automatically puts '0' in for any checkbox that
|
||||
// was not set, and the key to the checkbox if it is set.
|
||||
// Unfortunately, this means that if the key to that checkbox is 0,
|
||||
// we are unable to tell if that checkbox was set or not.
|
||||
|
||||
// Luckily, the '#value' on the checkboxes form actually contains
|
||||
// *only* a list of checkboxes that were set, and we can use that
|
||||
// instead.
|
||||
|
||||
$form_state['values']['options']['value'] = $form['value']['#value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a form for setting options.
|
||||
*/
|
||||
public function value_form(&$form, &$form_state) {
|
||||
$options = array();
|
||||
if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
|
||||
$options += $this->reduce_value_options($form_state);
|
||||
}
|
||||
else {
|
||||
$options += $this->definition['options'];
|
||||
}
|
||||
$form['value'] = array(
|
||||
'#type' => $this->value_form_type,
|
||||
'#title' => empty($form_state['exposed']) ? t('Value') : '',
|
||||
'#options' => $options,
|
||||
'#multiple' => TRUE,
|
||||
'#size' => min(4, count($this->definition['options'])),
|
||||
'#default_value' => isset($this->value) ? $this->value : array(),
|
||||
);
|
||||
|
||||
// Hide the value box if operator is 'empty' or 'not empty'.
|
||||
// Radios share the same selector so we have to add some dummy selector.
|
||||
// #states replace #dependency (http://drupal.org/node/1595022).
|
||||
$form['value']['#states']['visible'] = array(
|
||||
':input[name="options[operator]"],dummy-empty' => array('!value' => 'empty'),
|
||||
':input[name="options[operator]"],dummy-not-empty' => array('!value' => 'not empty'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a summary of this filter's value for the admin UI.
|
||||
*/
|
||||
public function admin_summary() {
|
||||
if (!empty($this->options['exposed'])) {
|
||||
return t('exposed');
|
||||
}
|
||||
|
||||
if ($this->operator === 'empty') {
|
||||
return t('is empty');
|
||||
}
|
||||
if ($this->operator === 'not empty') {
|
||||
return t('is not empty');
|
||||
}
|
||||
|
||||
if (!is_array($this->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$operator_options = $this->operator_options();
|
||||
$operator = $operator_options[$this->operator];
|
||||
$values = '';
|
||||
|
||||
// Remove every element which is not known.
|
||||
foreach ($this->value as $i => $value) {
|
||||
if (!isset($this->definition['options'][$value])) {
|
||||
unset($this->value[$i]);
|
||||
}
|
||||
}
|
||||
// Choose different kind of ouput for 0, a single and multiple values.
|
||||
if (count($this->value) == 0) {
|
||||
return $this->operator == '=' ? t('none') : t('any');
|
||||
}
|
||||
elseif (count($this->value) == 1) {
|
||||
// If there is only a single value, use just the plain operator, = or <>.
|
||||
$operator = check_plain($this->operator);
|
||||
$values = check_plain($this->definition['options'][reset($this->value)]);
|
||||
}
|
||||
else {
|
||||
foreach ($this->value as $value) {
|
||||
if ($values !== '') {
|
||||
$values .= ', ';
|
||||
}
|
||||
if (drupal_strlen($values) > 20) {
|
||||
$values .= '…';
|
||||
break;
|
||||
}
|
||||
$values .= check_plain($this->definition['options'][$value]);
|
||||
}
|
||||
}
|
||||
|
||||
return $operator . (($values !== '') ? ' ' . $values : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this filter to the query.
|
||||
*/
|
||||
public function query() {
|
||||
if ($this->operator === 'empty') {
|
||||
$this->query->condition($this->real_field, NULL, '=', $this->options['group']);
|
||||
}
|
||||
elseif ($this->operator === 'not empty') {
|
||||
$this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
|
||||
}
|
||||
else {
|
||||
while (is_array($this->value) && count($this->value) == 1) {
|
||||
$this->value = reset($this->value);
|
||||
}
|
||||
if (is_scalar($this->value) && $this->value !== '') {
|
||||
$this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
|
||||
}
|
||||
elseif ($this->value) {
|
||||
if ($this->operator == '=') {
|
||||
$filter = $this->query->createFilter('OR');
|
||||
// $filter will be NULL if there were errors in the query.
|
||||
if ($filter) {
|
||||
foreach ($this->value as $v) {
|
||||
$filter->condition($this->real_field, $v, '=');
|
||||
}
|
||||
$this->query->filter($filter, $this->options['group']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($this->value as $v) {
|
||||
$this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
15
contrib/search_api_views/includes/handler_filter_text.inc
Normal file
15
contrib/search_api_views/includes/handler_filter_text.inc
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views filter handler class for handling fulltext fields.
|
||||
*/
|
||||
class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter {
|
||||
|
||||
/**
|
||||
* Provide a list of options for the operator form.
|
||||
*/
|
||||
public function operator_options() {
|
||||
return array('=' => t('contains'), '<>' => t("doesn't contain"));
|
||||
}
|
||||
|
||||
}
|
30
contrib/search_api_views/includes/handler_sort.inc
Normal file
30
contrib/search_api_views/includes/handler_sort.inc
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class for sorting results according to a specified field.
|
||||
*/
|
||||
class SearchApiViewsHandlerSort extends views_handler_sort {
|
||||
|
||||
/**
|
||||
* The associated views query object.
|
||||
*
|
||||
* @var SearchApiViewsQuery
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* Called to add the sort to a query.
|
||||
*/
|
||||
public function query() {
|
||||
// When there are exposed sorts, the "exposed form" plugin will set
|
||||
// $query->orderby to an empty array. Therefore, if that property is set,
|
||||
// we here remove all previous sorts.
|
||||
if (isset($this->query->orderby)) {
|
||||
unset($this->query->orderby);
|
||||
$sort = &$this->query->getSort();
|
||||
$sort = array();
|
||||
}
|
||||
$this->query->sort($this->real_field, $this->options['order']);
|
||||
}
|
||||
|
||||
}
|
564
contrib/search_api_views/includes/query.inc
Executable file
564
contrib/search_api_views/includes/query.inc
Executable file
@ -0,0 +1,564 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Views query class using a Search API index as the data source.
|
||||
*/
|
||||
class SearchApiViewsQuery extends views_plugin_query {
|
||||
|
||||
/**
|
||||
* Number of results to display.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $limit;
|
||||
|
||||
/**
|
||||
* Offset of first displayed result.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $offset;
|
||||
|
||||
/**
|
||||
* The index this view accesses.
|
||||
*
|
||||
* @var SearchApiIndex
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* The query that will be executed.
|
||||
*
|
||||
* @var SearchApiQueryInterface
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* The results returned by the query, after it was executed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $search_api_results = array();
|
||||
|
||||
/**
|
||||
* Array of all encountered errors.
|
||||
*
|
||||
* Each of these is fatal, meaning that a non-empty $errors property will
|
||||
* result in an empty result being returned.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $errors;
|
||||
|
||||
/**
|
||||
* The names of all fields whose value is required by a handler.
|
||||
*
|
||||
* The format follows the same as Search API field identifiers (parent:child).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* The query's sub-filters representing the different Views filter groups.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $filters = array();
|
||||
|
||||
/**
|
||||
* The conjunction with which multiple filter groups are combined.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $group_operator = 'AND';
|
||||
|
||||
/**
|
||||
* Create the basic query object and fill with default values.
|
||||
*/
|
||||
public function init($base_table, $base_field, $options) {
|
||||
try {
|
||||
$this->errors = array();
|
||||
parent::init($base_table, $base_field, $options);
|
||||
$this->fields = array();
|
||||
if (substr($base_table, 0, 17) == 'search_api_index_') {
|
||||
$id = substr($base_table, 17);
|
||||
$this->index = search_api_index_load($id);
|
||||
$this->query = $this->index->query(array(
|
||||
'parse mode' => 'terms',
|
||||
));
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field that should be retrieved from the results by this view.
|
||||
*
|
||||
* @param $field
|
||||
* The field's identifier, as used by the Search API. E.g., "title" for a
|
||||
* node's title, "author:name" for a node's author's name.
|
||||
*
|
||||
* @return SearchApiViewsQuery
|
||||
* The called object.
|
||||
*/
|
||||
public function addField($field) {
|
||||
$this->fields[$field] = TRUE;
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sort to the query.
|
||||
*
|
||||
* @param $selector
|
||||
* The field to sort on. All indexed fields of the index are valid values.
|
||||
* In addition, the special fields 'search_api_relevance' (sort by
|
||||
* relevance) and 'search_api_id' (sort by item id) may be used.
|
||||
* @param $order
|
||||
* The order to sort items in - either 'ASC' or 'DESC'. Defaults to 'ASC'.
|
||||
*/
|
||||
public function add_selector_orderby($selector, $order = 'ASC') {
|
||||
$this->query->sort($selector, $order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the options used by this query plugin.
|
||||
*
|
||||
* Adds an option to bypass access checks.
|
||||
*/
|
||||
public function option_definition() {
|
||||
return parent::option_definition() + array(
|
||||
'search_api_bypass_access' => array(
|
||||
'default' => FALSE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add settings for the UI.
|
||||
*
|
||||
* Adds an option for bypassing access checks.
|
||||
*/
|
||||
public function options_form(&$form, &$form_state) {
|
||||
parent::options_form($form, $form_state);
|
||||
|
||||
$form['search_api_bypass_access'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Bypass access checks'),
|
||||
'#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
|
||||
'#default_value' => $this->options['search_api_bypass_access'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the necessary info to execute the query.
|
||||
*/
|
||||
public function build(&$view) {
|
||||
$this->view = $view;
|
||||
|
||||
// Setup the nested filter structure for this query.
|
||||
if (!empty($this->where)) {
|
||||
// If the different groups are combined with the OR operator, we have to
|
||||
// add a new OR filter to the query to which the filters for the groups
|
||||
// will be added.
|
||||
if ($this->group_operator === 'OR') {
|
||||
$base = $this->query->createFilter('OR');
|
||||
$this->query->filter($base);
|
||||
}
|
||||
else {
|
||||
$base = $this->query;
|
||||
}
|
||||
// Add a nested filter for each filter group, with its set conjunction.
|
||||
foreach ($this->where as $group_id => $group) {
|
||||
if (!empty($group['conditions']) || !empty($group['filters'])) {
|
||||
// For filters without a group, we want to always add them directly to
|
||||
// the query.
|
||||
$filter = ($group_id === '') ? $this->query : $this->query->createFilter($group['type']);
|
||||
if (!empty($group['conditions'])) {
|
||||
foreach ($group['conditions'] as $condition) {
|
||||
list($field, $value, $operator) = $condition;
|
||||
$filter->condition($field, $value, $operator);
|
||||
}
|
||||
}
|
||||
if (!empty($group['filters'])) {
|
||||
foreach ($group['filters'] as $nested_filter) {
|
||||
$filter->filter($nested_filter);
|
||||
}
|
||||
}
|
||||
// If no group was given, the filters were already set on the query.
|
||||
if ($group_id !== '') {
|
||||
$base->filter($filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the pager and let it modify the query to add limits.
|
||||
$view->init_pager();
|
||||
$this->pager->query();
|
||||
|
||||
// Add the "search_api_bypass_access" option to the query, if desired.
|
||||
if (!empty($this->options['search_api_bypass_access'])) {
|
||||
$this->query->setOption('search_api_bypass_access', TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query and fills the associated view object with according
|
||||
* values.
|
||||
*
|
||||
* Values to set: $view->result, $view->total_rows, $view->execute_time,
|
||||
* $view->pager['current_page'].
|
||||
*/
|
||||
public function execute(&$view) {
|
||||
if ($this->errors) {
|
||||
if (error_displayable()) {
|
||||
foreach ($this->errors as $msg) {
|
||||
drupal_set_message(check_plain($msg), 'error');
|
||||
}
|
||||
}
|
||||
$view->result = array();
|
||||
$view->total_rows = 0;
|
||||
$view->execute_time = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$start = microtime(TRUE);
|
||||
// Add range and search ID (if it wasn't already set).
|
||||
$this->query->range($this->offset, $this->limit);
|
||||
if ($this->query->getOption('search id') == get_class($this->query)) {
|
||||
$this->query->setOption('search id', 'search_api_views:' . $view->name . ':' . $view->current_display);
|
||||
}
|
||||
|
||||
// Execute the search.
|
||||
$results = $this->query->execute();
|
||||
$this->search_api_results = $results;
|
||||
|
||||
// Store the results.
|
||||
$this->pager->total_items = $view->total_rows = $results['result count'];
|
||||
if (!empty($this->pager->options['offset'])) {
|
||||
$this->pager->total_items -= $this->pager->options['offset'];
|
||||
}
|
||||
$this->pager->update_page_info();
|
||||
$view->result = array();
|
||||
if (!empty($results['results'])) {
|
||||
$this->addResults($results['results'], $view);
|
||||
}
|
||||
// We shouldn't use $results['performance']['complete'] here, since
|
||||
// extracting the results probably takes considerable time as well.
|
||||
$view->execute_time = microtime(TRUE) - $start;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->errors[] = $e->getMessage();
|
||||
// Recursion to get the same error behaviour as above.
|
||||
return $this->execute($view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for adding results to a view in the format expected by the
|
||||
* view.
|
||||
*/
|
||||
protected function addResults(array $results, $view) {
|
||||
$rows = array();
|
||||
$missing = array();
|
||||
$items = array();
|
||||
|
||||
// First off, we try to gather as much field values as possible without
|
||||
// loading any items.
|
||||
foreach ($results as $id => $result) {
|
||||
$row = array();
|
||||
|
||||
// Include the loaded item for this result row, if present, or the item
|
||||
// ID.
|
||||
if (!empty($result['entity'])) {
|
||||
$row['entity'] = $result['entity'];
|
||||
}
|
||||
else {
|
||||
$row['entity'] = $id;
|
||||
}
|
||||
|
||||
$row['_entity_properties']['search_api_relevance'] = $result['score'];
|
||||
$row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
|
||||
|
||||
// Gather any fields from the search results.
|
||||
if (!empty($result['fields'])) {
|
||||
$row['_entity_properties'] += $result['fields'];
|
||||
}
|
||||
|
||||
// Check whether we need to extract any properties from the result item.
|
||||
$missing_fields = array_diff_key($this->fields, $row);
|
||||
if ($missing_fields) {
|
||||
$missing[$id] = $missing_fields;
|
||||
if (is_object($row['entity'])) {
|
||||
$items[$id] = $row['entity'];
|
||||
}
|
||||
else {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the row values for adding them to the Views result afterwards.
|
||||
$rows[$id] = (object) $row;
|
||||
}
|
||||
|
||||
// Load items of those rows which haven't got all field values, yet.
|
||||
if (!empty($ids)) {
|
||||
$items += $this->index->loadItems($ids);
|
||||
// $items now includes loaded items, and those already passed in the
|
||||
// search results.
|
||||
foreach ($items as $id => $item) {
|
||||
// Extract item properties.
|
||||
$wrapper = $this->index->entityWrapper($item, FALSE);
|
||||
$rows[$id]->_entity_properties += $this->extractFields($wrapper, $missing[$id]);
|
||||
$rows[$id]->entity = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, add all rows to the Views result set.
|
||||
$view->result = array_values($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for extracting all necessary fields from a result item.
|
||||
*
|
||||
* Usually, this method isn't needed anymore as the properties are now
|
||||
* extracted by the field handlers themselves.
|
||||
*/
|
||||
protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
|
||||
$fields = array();
|
||||
foreach ($all_fields as $key => $true) {
|
||||
$fields[$key]['type'] = 'string';
|
||||
}
|
||||
$fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
|
||||
$ret = array();
|
||||
foreach ($all_fields as $key => $true) {
|
||||
$ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the according entity objects for the given query results.
|
||||
*
|
||||
* This is necessary to support generic entity handlers and plugins with this
|
||||
* query backend.
|
||||
*
|
||||
* If the current query isn't based on an entity type, the method will return
|
||||
* an empty array.
|
||||
*/
|
||||
public function get_result_entities($results, $relationship = NULL, $field = NULL) {
|
||||
list($type, $wrappers) = $this->get_result_wrappers($results, $relationship, $field);
|
||||
$return = array();
|
||||
foreach ($wrappers as $id => $wrapper) {
|
||||
try {
|
||||
$return[$id] = $wrapper->value();
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
return array($type, $return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the according metadata wrappers for the given query results.
|
||||
*
|
||||
* This is necessary to support generic entity handlers and plugins with this
|
||||
* query backend.
|
||||
*/
|
||||
public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
|
||||
$is_entity = (boolean) entity_get_info($this->index->item_type);
|
||||
$wrappers = array();
|
||||
$load_entities = array();
|
||||
foreach ($results as $row_index => $row) {
|
||||
if ($is_entity && isset($row->entity)) {
|
||||
// If this entity isn't load, register it for pre-loading.
|
||||
if (!is_object($row->entity)) {
|
||||
$load_entities[$row->entity] = $row_index;
|
||||
}
|
||||
|
||||
$wrappers[$row_index] = $this->index->entityWrapper($row->entity);
|
||||
}
|
||||
}
|
||||
|
||||
// If the results are entities, we pre-load them to make use of a multiple
|
||||
// load. (Otherwise, each result would be loaded individually.)
|
||||
if (!empty($load_entities)) {
|
||||
$entities = entity_load($this->index->item_type, array_keys($load_entities));
|
||||
foreach ($entities as $entity_id => $entity) {
|
||||
$wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the relationship, if necessary.
|
||||
$type = $this->index->item_type;
|
||||
$selector_suffix = '';
|
||||
if ($field && ($pos = strrpos($field, ':'))) {
|
||||
$selector_suffix = substr($field, 0, $pos);
|
||||
}
|
||||
if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
|
||||
// Use EntityFieldHandlerHelper to compute the correct data selector for
|
||||
// the relationship.
|
||||
$handler = (object) array(
|
||||
'view' => $this->view,
|
||||
'relationship' => $relationship,
|
||||
'real_field' => '',
|
||||
);
|
||||
$selector = EntityFieldHandlerHelper::construct_property_selector($handler);
|
||||
$selector .= ($selector ? ':' : '') . $selector_suffix;
|
||||
list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
|
||||
}
|
||||
|
||||
return array($type, $wrappers);
|
||||
}
|
||||
|
||||
/**
|
||||
* API function for accessing the raw Search API query object.
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* The search query object used internally by this handler.
|
||||
*/
|
||||
public function getSearchApiQuery() {
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* API function for accessing the raw Search API results.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as specified by
|
||||
* SearchApiQueryInterface::execute().
|
||||
*/
|
||||
public function getSearchApiResults() {
|
||||
return $this->search_api_results;
|
||||
}
|
||||
|
||||
//
|
||||
// Query interface methods (proxy to $this->query)
|
||||
//
|
||||
|
||||
public function createFilter($conjunction = 'AND') {
|
||||
if (!$this->errors) {
|
||||
return $this->query->createFilter($conjunction);
|
||||
}
|
||||
}
|
||||
|
||||
public function keys($keys = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->query->keys($keys);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function fields(array $fields) {
|
||||
if (!$this->errors) {
|
||||
$this->query->fields($fields);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a nested filter to the search query object.
|
||||
*
|
||||
* If $group is given, the filter is added to the relevant filter group
|
||||
* instead.
|
||||
*/
|
||||
public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->where[$group]['filters'][] = $filter;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a condition on the search query object.
|
||||
*
|
||||
* If $group is given, the condition is added to the relevant filter group
|
||||
* instead.
|
||||
*/
|
||||
public function condition($field, $value, $operator = '=', $group = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->where[$group]['conditions'][] = array($field, $value, $operator);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sort($field, $order = 'ASC') {
|
||||
if (!$this->errors) {
|
||||
$this->query->sort($field, $order);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function range($offset = NULL, $limit = NULL) {
|
||||
if (!$this->errors) {
|
||||
$this->query->range($offset, $limit);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIndex() {
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
public function &getKeys() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getKeys();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getOriginalKeys() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getOriginalKeys();
|
||||
}
|
||||
}
|
||||
|
||||
public function &getFields() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getFields();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getFilter() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getFilter();
|
||||
}
|
||||
}
|
||||
|
||||
public function &getSort() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getSort();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function getOption($name) {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getOption($name);
|
||||
}
|
||||
}
|
||||
|
||||
public function setOption($name, $value) {
|
||||
if (!$this->errors) {
|
||||
return $this->query->setOption($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function &getOptions() {
|
||||
if (!$this->errors) {
|
||||
return $this->query->getOptions();
|
||||
}
|
||||
$ret = NULL;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
30
contrib/search_api_views/search_api_views.info
Normal file
30
contrib/search_api_views/search_api_views.info
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
name = Search views
|
||||
description = Integrates the Search API with Views, enabling users to create views with searches as filters or arguments.
|
||||
dependencies[] = search_api
|
||||
dependencies[] = views
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
; Views handlers
|
||||
files[] = includes/display_facet_block.inc
|
||||
files[] = includes/handler_argument.inc
|
||||
files[] = includes/handler_argument_fulltext.inc
|
||||
files[] = includes/handler_argument_more_like_this.inc
|
||||
files[] = includes/handler_argument_text.inc
|
||||
files[] = includes/handler_filter.inc
|
||||
files[] = includes/handler_filter_boolean.inc
|
||||
files[] = includes/handler_filter_date.inc
|
||||
files[] = includes/handler_filter_fulltext.inc
|
||||
files[] = includes/handler_filter_language.inc
|
||||
files[] = includes/handler_filter_options.inc
|
||||
files[] = includes/handler_filter_text.inc
|
||||
files[] = includes/handler_sort.inc
|
||||
files[] = includes/query.inc
|
||||
|
||||
; Information added by drupal.org packaging script on 2013-01-09
|
||||
version = "7.x-1.4"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1357726719"
|
||||
|
97
contrib/search_api_views/search_api_views.install
Normal file
97
contrib/search_api_views/search_api_views.install
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the search_api_views module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Updates all Search API views to use the new, specification-compliant identifiers.
|
||||
*/
|
||||
function search_api_views_update_7101() {
|
||||
$tables = views_fetch_data();
|
||||
// Contains arrays with real fields mapped to field IDs for each table.
|
||||
$table_fields = array();
|
||||
foreach ($tables as $key => $table) {
|
||||
if (substr($key, 0, 17) != 'search_api_index_') {
|
||||
continue;
|
||||
}
|
||||
foreach ($table as $field => $info) {
|
||||
if (isset($info['real field']) && $field != $info['real field']) {
|
||||
$table_fields[$key][$info['real field']] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$table_fields) {
|
||||
return;
|
||||
}
|
||||
foreach (views_get_all_views() as $name => $view) {
|
||||
if (empty($view->base_table) || empty($table_fields[$view->base_table])) {
|
||||
continue;
|
||||
}
|
||||
$change = FALSE;
|
||||
$fields = $table_fields[$view->base_table];
|
||||
$change |= _search_api_views_update_7101_helper($view->base_field, $fields);
|
||||
if (!empty($view->display)) {
|
||||
foreach ($view->display as $key => &$display) {
|
||||
$options = &$display->display_options;
|
||||
if (isset($options['style_options']['grouping'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields);
|
||||
}
|
||||
if (isset($options['style_options']['columns'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['style_options']['columns'], $fields);
|
||||
}
|
||||
if (isset($options['style_options']['info'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['style_options']['info'], $fields);
|
||||
}
|
||||
if (isset($options['arguments'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['arguments'], $fields);
|
||||
}
|
||||
if (isset($options['fields'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['fields'], $fields);
|
||||
}
|
||||
if (isset($options['filters'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['filters'], $fields);
|
||||
}
|
||||
if (isset($options['sorts'])) {
|
||||
$change |= _search_api_views_update_7101_helper($options['sorts'], $fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($change) {
|
||||
$view->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for replacing field identifiers.
|
||||
*
|
||||
* @return
|
||||
* TRUE iff the identifier was changed.
|
||||
*/
|
||||
function _search_api_views_update_7101_helper(&$field, array $fields) {
|
||||
if (is_array($field)) {
|
||||
$change = FALSE;
|
||||
$new_field = array();
|
||||
foreach ($field as $k => $v) {
|
||||
$new_k = $k;
|
||||
$change |= _search_api_views_update_7101_helper($new_k, $fields);
|
||||
$change |= _search_api_views_update_7101_helper($v, $fields);
|
||||
$new_field[$new_k] = $v;
|
||||
}
|
||||
$field = $new_field;
|
||||
return $change;
|
||||
}
|
||||
if (isset($fields[$field])) {
|
||||
$field = $fields[$field];
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the now unnecessary "search_api_views_max_fields_depth" variable.
|
||||
*/
|
||||
function search_api_views_update_7102() {
|
||||
variable_del('search_api_views_max_fields_depth');
|
||||
}
|
52
contrib/search_api_views/search_api_views.module
Normal file
52
contrib/search_api_views/search_api_views.module
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements hook_views_api().
|
||||
*/
|
||||
function search_api_views_views_api() {
|
||||
return array(
|
||||
'api' => '3.0-alpha1',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_index_insert().
|
||||
*/
|
||||
function search_api_views_search_api_index_insert(SearchApiIndex $index) {
|
||||
// Make the new index available for views.
|
||||
views_invalidate_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_index_update().
|
||||
*/
|
||||
function search_api_views_search_api_index_update(SearchApiIndex $index) {
|
||||
if (!$index->enabled && $index->original->enabled) {
|
||||
_search_api_views_index_unavailable($index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_index_delete().
|
||||
*/
|
||||
function search_api_views_search_api_index_delete(SearchApiIndex $index) {
|
||||
_search_api_views_index_unavailable($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for reacting to a disabled or deleted search index.
|
||||
*/
|
||||
function _search_api_views_index_unavailable(SearchApiIndex $index) {
|
||||
$names = array();
|
||||
$table = 'search_api_index_' . $index->machine_name;
|
||||
foreach (views_get_all_views() as $name => $view) {
|
||||
if (empty($view->disabled) && $view->base_table == $table) {
|
||||
$names[] = $name;
|
||||
// @todo: if ($index_deleted) $view->delete()?
|
||||
}
|
||||
}
|
||||
if ($names) {
|
||||
views_invalidate_cache();
|
||||
drupal_set_message(t('The following views were using the index %name: @views. You should disable or delete them.', array('%name' => $index->name, '@views' => implode(', ', $names))), 'warning');
|
||||
}
|
||||
}
|
196
contrib/search_api_views/search_api_views.views.inc
Normal file
196
contrib/search_api_views/search_api_views.views.inc
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements hook_views_data().
|
||||
*/
|
||||
function search_api_views_views_data() {
|
||||
try {
|
||||
$data = array();
|
||||
$entity_types = entity_get_info();
|
||||
foreach (search_api_index_load_multiple(FALSE) as $index) {
|
||||
// Fill in base data.
|
||||
$key = 'search_api_index_' . $index->machine_name;
|
||||
$table = &$data[$key];
|
||||
$type_info = search_api_get_item_type_info($index->item_type);
|
||||
$table['table']['group'] = t('Indexed @entity_type', array('@entity_type' => $type_info['name']));
|
||||
$table['table']['base'] = array(
|
||||
'field' => 'search_api_id',
|
||||
'index' => $index->machine_name,
|
||||
'title' => $index->name,
|
||||
'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)),
|
||||
'query class' => 'search_api_views_query',
|
||||
);
|
||||
if (isset($entity_types[$index->item_type])) {
|
||||
$table['table'] += array(
|
||||
'entity type' => $index->item_type,
|
||||
'skip entity load' => TRUE,
|
||||
);
|
||||
}
|
||||
|
||||
$wrapper = $index->entityWrapper(NULL, TRUE);
|
||||
|
||||
// Add field handlers and relationships provided by the Entity API.
|
||||
foreach ($wrapper as $key => $property) {
|
||||
$info = $property->info();
|
||||
if ($info) {
|
||||
entity_views_field_definition($key, $info, $table);
|
||||
}
|
||||
}
|
||||
|
||||
// Add handlers for all indexed fields.
|
||||
foreach ($index->getFields() as $key => $field) {
|
||||
$tmp = $wrapper;
|
||||
$group = '';
|
||||
$name = '';
|
||||
$parts = explode(':', $key);
|
||||
foreach ($parts as $i => $part) {
|
||||
if (!isset($tmp->$part)) {
|
||||
continue 2;
|
||||
}
|
||||
$tmp = $tmp->$part;
|
||||
$info = $tmp->info();
|
||||
$group = ($group ? $group . ' » ' . $name : ($name ? $name : ''));
|
||||
$name = $info['label'];
|
||||
if ($i < count($parts) - 1) {
|
||||
// Unwrap lists.
|
||||
$level = search_api_list_nesting_level($info['type']);
|
||||
for ($j = 0; $j < $level; ++$j) {
|
||||
$tmp = $tmp[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
$id = _entity_views_field_identifier($key, $table);
|
||||
if ($group) {
|
||||
// @todo Entity type label instead of $group?
|
||||
$table[$id]['group'] = $group;
|
||||
$name = t('@field (indexed)', array('@field' => $name));
|
||||
}
|
||||
$table[$id]['title'] = $name;
|
||||
$table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description'];
|
||||
$table[$id]['type'] = $field['type'];
|
||||
if ($id != $key) {
|
||||
$table[$id]['real field'] = $key;
|
||||
}
|
||||
_search_api_views_add_handlers($key, $field, $tmp, $table);
|
||||
}
|
||||
|
||||
// Special handlers
|
||||
$table['search_api_language']['filter']['handler'] = 'SearchApiViewsHandlerFilterLanguage';
|
||||
|
||||
$table['search_api_id']['title'] = t('Entity ID');
|
||||
$table['search_api_id']['help'] = t("The entity's ID.");
|
||||
$table['search_api_id']['sort']['handler'] = 'SearchApiViewsHandlerSort';
|
||||
|
||||
$table['search_api_relevance']['group'] = t('Search');
|
||||
$table['search_api_relevance']['title'] = t('Relevance');
|
||||
$table['search_api_relevance']['help'] = t('The relevance of this search result with respect to the query.');
|
||||
$table['search_api_relevance']['field']['type'] = 'decimal';
|
||||
$table['search_api_relevance']['field']['handler'] = 'entity_views_handler_field_numeric';
|
||||
$table['search_api_relevance']['field']['click sortable'] = TRUE;
|
||||
$table['search_api_relevance']['sort']['handler'] = 'SearchApiViewsHandlerSort';
|
||||
|
||||
$table['search_api_excerpt']['group'] = t('Search');
|
||||
$table['search_api_excerpt']['title'] = t('Excerpt');
|
||||
$table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms.');
|
||||
$table['search_api_excerpt']['field']['type'] = 'text';
|
||||
$table['search_api_excerpt']['field']['handler'] = 'entity_views_handler_field_text';
|
||||
|
||||
$table['search_api_views_fulltext']['group'] = t('Search');
|
||||
$table['search_api_views_fulltext']['title'] = t('Fulltext search');
|
||||
$table['search_api_views_fulltext']['help'] = t('Search several or all fulltext fields at once.');
|
||||
$table['search_api_views_fulltext']['filter']['handler'] = 'SearchApiViewsHandlerFilterFulltext';
|
||||
$table['search_api_views_fulltext']['argument']['handler'] = 'SearchApiViewsHandlerArgumentFulltext';
|
||||
|
||||
$table['search_api_views_more_like_this']['group'] = t('Search');
|
||||
$table['search_api_views_more_like_this']['title'] = t('More like this');
|
||||
$table['search_api_views_more_like_this']['help'] = t('Find similar content.');
|
||||
$table['search_api_views_more_like_this']['argument']['handler'] = 'SearchApiViewsHandlerArgumentMoreLikeThis';
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
watchdog_exception('search_api_views', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that returns an array of handler definitions to add to a
|
||||
* views field definition.
|
||||
*/
|
||||
function _search_api_views_add_handlers($id, array $field, EntityMetadataWrapper $wrapper, array &$table) {
|
||||
$type = $field['type'];
|
||||
$inner_type = search_api_extract_inner_type($type);
|
||||
|
||||
if (strpos($id, ':')) {
|
||||
entity_views_field_definition($id, $wrapper->info(), $table);
|
||||
}
|
||||
$id = _entity_views_field_identifier($id, $table);
|
||||
$table += array($id => array());
|
||||
|
||||
if ($inner_type == 'text') {
|
||||
$table[$id] += array(
|
||||
'argument' => array(
|
||||
'handler' => 'SearchApiViewsHandlerArgumentText',
|
||||
),
|
||||
'filter' => array(
|
||||
'handler' => 'SearchApiViewsHandlerFilterText',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options = $wrapper->optionsList('view')) {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
|
||||
$table[$id]['filter']['options'] = $options;
|
||||
}
|
||||
elseif ($inner_type == 'boolean') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterBoolean';
|
||||
}
|
||||
elseif ($inner_type == 'date') {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
|
||||
}
|
||||
else {
|
||||
$table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
|
||||
}
|
||||
|
||||
$table[$id]['argument']['handler'] = 'SearchApiViewsHandlerArgument';
|
||||
|
||||
// We can only sort according to single-valued fields.
|
||||
if ($type == $inner_type) {
|
||||
$table[$id]['sort']['handler'] = 'SearchApiViewsHandlerSort';
|
||||
if (isset($table[$id]['field'])) {
|
||||
$table[$id]['field']['click sortable'] = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_views_plugins().
|
||||
*/
|
||||
function search_api_views_views_plugins() {
|
||||
$ret = array(
|
||||
'query' => array(
|
||||
'search_api_views_query' => array(
|
||||
'title' => t('Search API Query'),
|
||||
'help' => t('Query will be generated and run using the Search API.'),
|
||||
'handler' => 'SearchApiViewsQuery'
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (module_exists('search_api_facetapi')) {
|
||||
$ret['display']['search_api_views_facets_block'] = array(
|
||||
'title' => t('Facets block'),
|
||||
'help' => t('Display facets for this search as a block anywhere on the site.'),
|
||||
'handler' => 'SearchApiViewsFacetsBlockDisplay',
|
||||
'uses hook block' => TRUE,
|
||||
'use ajax' => FALSE,
|
||||
'use pager' => FALSE,
|
||||
'use more' => TRUE,
|
||||
'accept attachments' => TRUE,
|
||||
'admin' => t('Facets block'),
|
||||
);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
BIN
disabled.png
Normal file
BIN
disabled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 384 B |
BIN
enabled.png
Normal file
BIN
enabled.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 383 B |
220
includes/callback.inc
Normal file
220
includes/callback.inc
Normal file
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Interface representing a Search API data-alter callback.
|
||||
*/
|
||||
interface SearchApiAlterCallbackInterface {
|
||||
|
||||
/**
|
||||
* Construct a data-alter callback.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose items will be altered.
|
||||
* @param array $options
|
||||
* The callback options set for this index.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array());
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* This can be used for hiding the callback on the index's "Workflow" tab. To
|
||||
* avoid confusion, you should only use criteria that are immutable, such as
|
||||
* the index's entity type. Also, since this is only used for UI purposes, you
|
||||
* should not completely rely on this to ensure certain index configurations
|
||||
* and at least throw an exception with a descriptive error message if this is
|
||||
* violated on runtime.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Display a form for configuring this callback.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this callback, or FALSE if no configuration
|
||||
* is possible.
|
||||
*/
|
||||
public function configurationForm();
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing. This could for instance be used to implement
|
||||
* some sort of access filter for security purposes (e.g., don't index
|
||||
* unpublished nodes or comments).
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items);
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this
|
||||
* callback. If a property with this name already exists for an entity it
|
||||
* will be overridden, so keep a clear namespace by prefixing the properties
|
||||
* with the module name if this is not desired.
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*
|
||||
* @return array
|
||||
* Information about all additional properties, as specified by
|
||||
* hook_entity_property_info() (only the inner "properties" array).
|
||||
*/
|
||||
public function propertyInfo();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for data-alter callbacks, implementing most methods with
|
||||
* sensible defaults.
|
||||
* Extending classes will at least have to implement the alterItems() method to
|
||||
* make this work. If that method adds additional fields to the items,
|
||||
* propertyInfo() has to be overridden, too.
|
||||
*/
|
||||
abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackInterface {
|
||||
|
||||
/**
|
||||
* The index whose items will be altered.
|
||||
*
|
||||
* @var SearchApiIndex
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* The configuration options for this callback, if it has any.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Construct a data-alter callback.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose items will be altered.
|
||||
* @param array $options
|
||||
* The callback options set for this index.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
$this->index = $index;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* This can be used for hiding the callback on the index's "Workflow" tab. To
|
||||
* avoid confusion, you should only use criteria that are immutable, such as
|
||||
* the index's entity type. Also, since this is only used for UI purposes, you
|
||||
* should not completely rely on this to ensure certain index configurations
|
||||
* and at least throw an exception with a descriptive error message if this is
|
||||
* violated on runtime.
|
||||
*
|
||||
* The default implementation always returns TRUE.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form for configuring this callback.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this callback, or FALSE if no configuration
|
||||
* is possible.
|
||||
*/
|
||||
public function configurationForm() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) { }
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this
|
||||
* callback. If a property with this name already exists for an entity it
|
||||
* will be overridden, so keep a clear namespace by prefixing the properties
|
||||
* with the module name if this is not desired.
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*
|
||||
* @return array
|
||||
* Information about all additional properties, as specified by
|
||||
* hook_entity_property_info() (only the inner "properties" array).
|
||||
*/
|
||||
public function propertyInfo() {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
313
includes/callback_add_aggregation.inc
Normal file
313
includes/callback_add_aggregation.inc
Normal file
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
|
||||
|
||||
public function configurationForm() {
|
||||
$form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
|
||||
|
||||
$fields = $this->index->getFields(FALSE);
|
||||
$field_options = array();
|
||||
foreach ($fields as $name => $field) {
|
||||
$field_options[$name] = $field['name'];
|
||||
}
|
||||
$additional = empty($this->options['fields']) ? array() : $this->options['fields'];
|
||||
|
||||
$types = $this->getTypes();
|
||||
$type_descriptions = $this->getTypes('description');
|
||||
$tmp = array();
|
||||
foreach ($types as $type => $name) {
|
||||
$tmp[$type] = array(
|
||||
'#type' => 'item',
|
||||
'#description' => $type_descriptions[$type],
|
||||
);
|
||||
}
|
||||
$type_descriptions = $tmp;
|
||||
|
||||
$form['#id'] = 'edit-callbacks-search-api-alter-add-aggregation-settings';
|
||||
$form['description'] = array(
|
||||
'#markup' => t('<p>This data alteration lets you define additional fields that will be added to this index. ' .
|
||||
'Each of these new fields will be an aggregation of one or more existing fields.</p>' .
|
||||
'<p>To add a new aggregated field, click the "Add new field" button and then fill out the form.</p>' .
|
||||
'<p>To remove a previously defined field, click the "Remove field" button.</p>' .
|
||||
'<p>You can also change the names or contained fields of existing aggregated fields.</p>'),
|
||||
);
|
||||
$form['fields']['#prefix'] = '<div id="search-api-alter-add-aggregation-field-settings">';
|
||||
$form['fields']['#suffix'] = '</div>';
|
||||
if (isset($this->changes)) {
|
||||
$form['fields']['#prefix'] .= '<div class="messages warning">All changes in the form will not be saved until the <em>Save configuration</em> button at the form bottom is clicked.</div>';
|
||||
}
|
||||
foreach ($additional as $name => $field) {
|
||||
$form['fields'][$name] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $field['name'] ? $field['name'] : t('New field'),
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => (boolean) $field['name'],
|
||||
);
|
||||
$form['fields'][$name]['name'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('New field name'),
|
||||
'#default_value' => $field['name'],
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['fields'][$name]['type'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Aggregation type'),
|
||||
'#options' => $types,
|
||||
'#default_value' => $field['type'],
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['fields'][$name]['type_descriptions'] = $type_descriptions;
|
||||
foreach (array_keys($types) as $type) {
|
||||
$form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type;
|
||||
}
|
||||
$form['fields'][$name]['fields'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Contained fields'),
|
||||
'#options' => $field_options,
|
||||
'#default_value' => drupal_map_assoc($field['fields']),
|
||||
'#attributes' => array('class' => array('search-api-alter-add-aggregation-fields')),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['fields'][$name]['actions'] = array(
|
||||
'#type' => 'actions',
|
||||
'remove' => array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Remove field'),
|
||||
'#submit' => array('_search_api_add_aggregation_field_submit'),
|
||||
'#limit_validation_errors' => array(),
|
||||
'#name' => 'search_api_add_aggregation_remove_' . $name,
|
||||
'#ajax' => array(
|
||||
'callback' => '_search_api_add_aggregation_field_ajax',
|
||||
'wrapper' => 'search-api-alter-add-aggregation-field-settings',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
$form['actions']['#type'] = 'actions';
|
||||
$form['actions']['add_field'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Add new field'),
|
||||
'#submit' => array('_search_api_add_aggregation_field_submit'),
|
||||
'#limit_validation_errors' => array(),
|
||||
'#ajax' => array(
|
||||
'callback' => '_search_api_add_aggregation_field_ajax',
|
||||
'wrapper' => 'search-api-alter-add-aggregation-field-settings',
|
||||
),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
unset($values['actions']);
|
||||
if (empty($values['fields'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($values['fields'] as $name => $field) {
|
||||
$fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
|
||||
unset($values['fields'][$name]['actions']);
|
||||
if ($field['name'] && !$fields) {
|
||||
form_error($form['fields'][$name]['fields'], t('You have to select at least one field to aggregate. If you want to remove an aggregated field, please delete its name.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
if (empty($values['fields'])) {
|
||||
return array();
|
||||
}
|
||||
$index_fields = $this->index->getFields(FALSE);
|
||||
foreach ($values['fields'] as $name => $field) {
|
||||
if (!$field['name']) {
|
||||
unset($values['fields'][$name]);
|
||||
}
|
||||
else {
|
||||
$values['fields'][$name]['description'] = $this->fieldDescription($field, $index_fields);
|
||||
}
|
||||
}
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
if (!$items) {
|
||||
return;
|
||||
}
|
||||
if (isset($this->options['fields'])) {
|
||||
$types = $this->getTypes('type');
|
||||
foreach ($items as $item) {
|
||||
$wrapper = $this->index->entityWrapper($item);
|
||||
foreach ($this->options['fields'] as $name => $field) {
|
||||
if ($field['name']) {
|
||||
$required_fields = array();
|
||||
foreach ($field['fields'] as $f) {
|
||||
if (!isset($required_fields[$f])) {
|
||||
$required_fields[$f]['type'] = $types[$field['type']];
|
||||
}
|
||||
}
|
||||
$fields = search_api_extract_fields($wrapper, $required_fields);
|
||||
$values = array();
|
||||
foreach ($fields as $f) {
|
||||
if (isset($f['value'])) {
|
||||
$values[] = $f['value'];
|
||||
}
|
||||
}
|
||||
$values = $this->flattenArray($values);
|
||||
|
||||
$this->reductionType = $field['type'];
|
||||
$item->$name = array_reduce($values, array($this, 'reduce'), NULL);
|
||||
if ($field['type'] == 'count' && !$item->$name) {
|
||||
$item->$name = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for reducing an array to a single value.
|
||||
*/
|
||||
public function reduce($a, $b) {
|
||||
switch ($this->reductionType) {
|
||||
case 'fulltext':
|
||||
return isset($a) ? $a . "\n\n" . $b : $b;
|
||||
case 'sum':
|
||||
return $a + $b;
|
||||
case 'count':
|
||||
return $a + 1;
|
||||
case 'max':
|
||||
return isset($a) ? max($a, $b) : $b;
|
||||
case 'min':
|
||||
return isset($a) ? min($a, $b) : $b;
|
||||
case 'first':
|
||||
return isset($a) ? $a : $b;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for flattening a multi-dimensional array.
|
||||
*/
|
||||
protected function flattenArray(array $data) {
|
||||
$ret = array();
|
||||
foreach ($data as $item) {
|
||||
if (!isset($item)) {
|
||||
continue;
|
||||
}
|
||||
if (is_scalar($item)) {
|
||||
$ret[] = $item;
|
||||
}
|
||||
else {
|
||||
$ret = array_merge($ret, $this->flattenArray($item));
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function propertyInfo() {
|
||||
$types = $this->getTypes('type');
|
||||
$ret = array();
|
||||
if (isset($this->options['fields'])) {
|
||||
foreach ($this->options['fields'] as $name => $field) {
|
||||
$ret[$name] = array(
|
||||
'label' => $field['name'],
|
||||
'description' => empty($field['description']) ? '' : $field['description'],
|
||||
'type' => $types[$field['type']],
|
||||
);
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for creating a field description.
|
||||
*/
|
||||
protected function fieldDescription(array $field, array $index_fields) {
|
||||
$fields = array();
|
||||
foreach ($field['fields'] as $f) {
|
||||
$fields[] = isset($index_fields[$f]) ? $index_fields[$f]['name'] : $f;
|
||||
}
|
||||
$type = $this->getTypes();
|
||||
$type = $type[$field['type']];
|
||||
return t('A @type aggregation of the following fields: @fields.', array('@type' => $type, '@fields' => implode(', ', $fields)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for getting all available aggregation types.
|
||||
*
|
||||
* @param $info (optional)
|
||||
* One of "name", "type" or "description", to indicate what values should be
|
||||
* returned for the types. Defaults to "name".
|
||||
*
|
||||
*/
|
||||
protected function getTypes($info = 'name') {
|
||||
switch ($info) {
|
||||
case 'name':
|
||||
return array(
|
||||
'fulltext' => t('Fulltext'),
|
||||
'sum' => t('Sum'),
|
||||
'count' => t('Count'),
|
||||
'max' => t('Maximum'),
|
||||
'min' => t('Minimum'),
|
||||
'first' => t('First'),
|
||||
);
|
||||
case 'type':
|
||||
return array(
|
||||
'fulltext' => 'text',
|
||||
'sum' => 'integer',
|
||||
'count' => 'integer',
|
||||
'max' => 'integer',
|
||||
'min' => 'integer',
|
||||
'first' => 'string',
|
||||
);
|
||||
case 'description':
|
||||
return array(
|
||||
'fulltext' => t('The Fulltext aggregation concatenates the text data of all contained fields.'),
|
||||
'sum' => t('The Sum aggregation adds the values of all contained fields numerically.'),
|
||||
'count' => t('The Count aggregation takes the total number of contained field values as the aggregated field value.'),
|
||||
'max' => t('The Maximum aggregation computes the numerically largest contained field value.'),
|
||||
'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'),
|
||||
'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit helper callback for buttons in the callback's configuration form.
|
||||
*/
|
||||
public function formButtonSubmit(array $form, array &$form_state) {
|
||||
$button_name = $form_state['triggering_element']['#name'];
|
||||
if ($button_name == 'op') {
|
||||
for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
|
||||
}
|
||||
$this->options['fields']['search_api_aggregation_' . $i] = array(
|
||||
'name' => '',
|
||||
'type' => 'fulltext',
|
||||
'fields' => array(),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$field = substr($button_name, 34);
|
||||
unset($this->options['fields'][$field]);
|
||||
}
|
||||
$form_state['rebuild'] = TRUE;
|
||||
$this->changes = TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit function for buttons in the callback's configuration form.
|
||||
*/
|
||||
function _search_api_add_aggregation_field_submit(array $form, array &$form_state) {
|
||||
$form_state['callbacks']['search_api_alter_add_aggregation']->formButtonSubmit($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX submit function for buttons in the callback's configuration form.
|
||||
*/
|
||||
function _search_api_add_aggregation_field_ajax(array $form, array &$form_state) {
|
||||
return $form['callbacks']['settings']['search_api_alter_add_aggregation']['fields'];
|
||||
}
|
243
includes/callback_add_hierarchy.inc
Normal file
243
includes/callback_add_hierarchy.inc
Normal file
@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Cached value for the hierarchical field options.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see getHierarchicalFields()
|
||||
*/
|
||||
protected $field_options;
|
||||
|
||||
/**
|
||||
* Enable this data alteration only if any hierarchical fields are available.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return (bool) $this->getHierarchicalFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form for configuring this callback.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this callback, or FALSE if no configuration
|
||||
* is possible.
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$options = $this->getHierarchicalFields();
|
||||
$this->options += array('fields' => array());
|
||||
$form['fields'] = array(
|
||||
'#title' => t('Hierarchical fields'),
|
||||
'#description' => t('Select the fields which should be supplemented with their ancestors. ' .
|
||||
'Each field is listed along with its children of the same type. ' .
|
||||
'When selecting several child properties of a field, all those properties will be recursively added to that field. ' .
|
||||
'Please note that you should de-select all fields before disabling this data alteration.'),
|
||||
'#type' => 'select',
|
||||
'#multiple' => TRUE,
|
||||
'#size' => min(6, count($options, COUNT_RECURSIVE)),
|
||||
'#options' => $options,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
// Change the saved type of fields in the index, if necessary.
|
||||
if (!empty($this->index->options['fields'])) {
|
||||
$fields = &$this->index->options['fields'];
|
||||
$previous = drupal_map_assoc($this->options['fields']);
|
||||
foreach ($values['fields'] as $field) {
|
||||
list($key, $prop) = explode(':', $field);
|
||||
if (empty($previous[$field]) && isset($fields[$key]['type'])) {
|
||||
$fields[$key]['type'] = 'list<' . search_api_extract_inner_type($fields[$key]['type']) . '>';
|
||||
$change = TRUE;
|
||||
}
|
||||
}
|
||||
$new = drupal_map_assoc($values['fields']);
|
||||
foreach ($previous as $field) {
|
||||
list($key, $prop) = explode(':', $field);
|
||||
if (empty($new[$field]) && isset($fields[$key]['type'])) {
|
||||
$w = $this->index->entityWrapper(NULL, FALSE);
|
||||
if (isset($w->$key)) {
|
||||
$type = $w->$key->type();
|
||||
$inner = search_api_extract_inner_type($fields[$key]['type']);
|
||||
$fields[$key]['type'] = search_api_nest_type($inner, $type);
|
||||
$change = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($change)) {
|
||||
$this->index->save();
|
||||
}
|
||||
}
|
||||
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing. This could for instance be used to implement
|
||||
* some sort of access filter for security purposes (e.g., don't index
|
||||
* unpublished nodes or comments).
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
if (empty($this->options['fields'])) {
|
||||
return array();
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
$wrapper = $this->index->entityWrapper($item, FALSE);
|
||||
|
||||
$values = array();
|
||||
foreach ($this->options['fields'] as $field) {
|
||||
list($key, $prop) = explode(':', $field);
|
||||
if (!isset($wrapper->$key)) {
|
||||
continue;
|
||||
}
|
||||
$child = $wrapper->$key;
|
||||
|
||||
$values += array($key => array());
|
||||
$this->extractHierarchy($child, $prop, $values[$key]);
|
||||
}
|
||||
foreach ($values as $key => $value) {
|
||||
$item->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this
|
||||
* callback. If a property with this name already exists for an entity it
|
||||
* will be overridden, so keep a clear namespace by prefixing the properties
|
||||
* with the module name if this is not desired.
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*
|
||||
* @return array
|
||||
* Information about all additional properties, as specified by
|
||||
* hook_entity_property_info() (only the inner "properties" array).
|
||||
*/
|
||||
public function propertyInfo() {
|
||||
if (empty($this->options['fields'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$ret = array();
|
||||
$wrapper = $this->index->entityWrapper(NULL, FALSE);
|
||||
foreach ($this->options['fields'] as $field) {
|
||||
list($key, $prop) = explode(':', $field);
|
||||
if (!isset($wrapper->$key)) {
|
||||
continue;
|
||||
}
|
||||
$child = $wrapper->$key;
|
||||
while (search_api_is_list_type($child->type())) {
|
||||
$child = $child[0];
|
||||
}
|
||||
if (!isset($child->$prop)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($ret[$key])) {
|
||||
$ret[$key] = $child->info();
|
||||
$type = search_api_extract_inner_type($ret[$key]['type']);
|
||||
$ret[$key]['type'] = "list<$type>";
|
||||
$ret[$key]['getter callback'] = 'entity_property_verbatim_get';
|
||||
// The return value of info() has some additional internal values set,
|
||||
// which we have to unset for the use here.
|
||||
unset($ret[$key]['name'], $ret[$key]['parent'], $ret[$key]['langcode'], $ret[$key]['clear'],
|
||||
$ret[$key]['property info alter'], $ret[$key]['property defaults']);
|
||||
}
|
||||
if (isset($ret[$key]['bundle'])) {
|
||||
$info = $child->$prop->info();
|
||||
if (empty($info['bundle']) || $ret[$key]['bundle'] != $info['bundle']) {
|
||||
unset($ret[$key]['bundle']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for finding all hierarchical fields of an index's type.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all hierarchical fields of the index, structured as
|
||||
* an options array grouped by primary field.
|
||||
*/
|
||||
protected function getHierarchicalFields() {
|
||||
if (!isset($this->field_options)) {
|
||||
$this->field_options = array();
|
||||
$wrapper = $this->index->entityWrapper(NULL, FALSE);
|
||||
// Only entities can be indexed in hierarchies, as other properties don't
|
||||
// have IDs that we can extract and store.
|
||||
$entity_info = entity_get_info();
|
||||
foreach ($wrapper as $key1 => $child) {
|
||||
while (search_api_is_list_type($child->type())) {
|
||||
$child = $child[0];
|
||||
}
|
||||
$info = $child->info();
|
||||
$type = $child->type();
|
||||
if (empty($entity_info[$type])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($child as $key2 => $prop) {
|
||||
if (search_api_extract_inner_type($prop->type()) == $type) {
|
||||
$prop_info = $prop->info();
|
||||
$this->field_options[$info['label']]["$key1:$key2"] = $prop_info['label'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->field_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a hierarchy from a metadata wrapper by modifying $values.
|
||||
*/
|
||||
public function extractHierarchy(EntityMetadataWrapper $wrapper, $property, array &$values) {
|
||||
if (search_api_is_list_type($wrapper->type())) {
|
||||
foreach ($wrapper as $w) {
|
||||
$this->extractHierarchy($w, $property, $values);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$v = $wrapper->value(array('identifier' => TRUE));
|
||||
if ($v && !isset($values[$v])) {
|
||||
$values[$v] = $v;
|
||||
if (isset($wrapper->$property) && $wrapper->$property->value()) {
|
||||
$this->extractHierarchy($wrapper->$property, $property, $values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
29
includes/callback_add_url.inc
Normal file
29
includes/callback_add_url.inc
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback {
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
foreach ($items as $id => &$item) {
|
||||
$url = $this->index->datasource()->getItemUrl($item);
|
||||
if (!$url) {
|
||||
$item->search_api_url = NULL;
|
||||
continue;
|
||||
}
|
||||
$item->search_api_url = url($url['path'], array('absolute' => TRUE) + $url['options']);
|
||||
}
|
||||
}
|
||||
|
||||
public function propertyInfo() {
|
||||
return array(
|
||||
'search_api_url' => array(
|
||||
'label' => t('URI'),
|
||||
'description' => t('An URI where the item can be accessed.'),
|
||||
'type' => 'uri',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
98
includes/callback_add_viewed_entity.inc
Normal file
98
includes/callback_add_viewed_entity.inc
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that adds an URL field for all items.
|
||||
*/
|
||||
class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Only support indexes containing entities.
|
||||
*
|
||||
* @see SearchApiAlterCallbackInterface::supportsIndex()
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return (bool) entity_get_info($index->item_type);
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$info = entity_get_info($this->index->item_type);
|
||||
$view_modes = array();
|
||||
foreach ($info['view modes'] as $key => $mode) {
|
||||
$view_modes[$key] = $mode['label'];
|
||||
}
|
||||
$this->options += array('mode' => reset($view_modes));
|
||||
if (count($view_modes) > 1) {
|
||||
$form['mode'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('View mode'),
|
||||
'#options' => $view_modes,
|
||||
'#default_value' => $this->options['mode'],
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['mode'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $this->options['mode'],
|
||||
);
|
||||
if ($view_modes) {
|
||||
$form['note'] = array(
|
||||
'#markup' => '<p>' . t('Entities of type %type have only a single view mode. ' .
|
||||
'Therefore, no selection needs to be made.', array('%type' => $info['label'])) . '</p>',
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['note'] = array(
|
||||
'#markup' => '<p>' . t('Entities of type %type have no defined view modes. ' .
|
||||
'This might either mean that they are always displayed the same way, or that they cannot be processed by this alteration at all. ' .
|
||||
'Please consider this when using this alteration.', array('%type' => $info['label'])) . '</p>',
|
||||
);
|
||||
}
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
// Prevent session information from being saved while indexing.
|
||||
drupal_save_session(FALSE);
|
||||
|
||||
// Force the current user to anonymous to prevent access bypass in search
|
||||
// indexes.
|
||||
$original_user = $GLOBALS['user'];
|
||||
$GLOBALS['user'] = drupal_anonymous_user();
|
||||
|
||||
$type = $this->index->item_type;
|
||||
$mode = empty($this->options['mode']) ? 'full' : $this->options['mode'];
|
||||
foreach ($items as $id => &$item) {
|
||||
// Since we can't really know what happens in entity_view() and render(),
|
||||
// we use try/catch. This will at least prevent some errors, even though
|
||||
// it's no protection against fatal errors and the like.
|
||||
try {
|
||||
$render = entity_view($type, array(entity_id($type, $item) => $item), $mode);
|
||||
$text = render($render);
|
||||
if (!$text) {
|
||||
$item->search_api_viewed = NULL;
|
||||
continue;
|
||||
}
|
||||
$item->search_api_viewed = $text;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$item->search_api_viewed = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the user.
|
||||
$GLOBALS['user'] = $original_user;
|
||||
drupal_save_session(TRUE);
|
||||
}
|
||||
|
||||
public function propertyInfo() {
|
||||
return array(
|
||||
'search_api_viewed' => array(
|
||||
'label' => t('Entity HTML output'),
|
||||
'description' => t('The whole HTML content of the entity when viewed.'),
|
||||
'type' => 'text',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
72
includes/callback_bundle_filter.inc
Normal file
72
includes/callback_bundle_filter.inc
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that filters out items based on their
|
||||
* bundle.
|
||||
*/
|
||||
class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
|
||||
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return ($info = entity_get_info($index->item_type)) && self::hasBundles($info);
|
||||
}
|
||||
|
||||
public function alterItems(array &$items) {
|
||||
$info = entity_get_info($this->index->item_type);
|
||||
if (self::hasBundles($info) && isset($this->options['bundles'])) {
|
||||
$bundles = array_flip($this->options['bundles']);
|
||||
$default = (bool) $this->options['default'];
|
||||
$bundle_prop = $info['entity keys']['bundle'];
|
||||
foreach ($items as $id => $item) {
|
||||
if (isset($bundles[$item->$bundle_prop]) == $default) {
|
||||
unset($items[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$info = entity_get_info($this->index->item_type);
|
||||
if (self::hasBundles($info)) {
|
||||
$options = array();
|
||||
foreach ($info['bundles'] as $bundle => $bundle_info) {
|
||||
$options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
|
||||
}
|
||||
$form = array(
|
||||
'default' => array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Which items should be indexed?'),
|
||||
'#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
|
||||
'#options' => array(
|
||||
1 => t('All but those from one of the selected bundles'),
|
||||
0 => t('Only those from the selected bundles'),
|
||||
),
|
||||
),
|
||||
'bundles' => array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Bundles'),
|
||||
'#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(),
|
||||
'#options' => $options,
|
||||
'#size' => min(4, count($options)),
|
||||
'#multiple' => TRUE,
|
||||
),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form = array(
|
||||
'forbidden' => array(
|
||||
'#markup' => '<p>' . t("Items indexed by this index don't have bundles and therefore cannot be filtered here.") . '</p>',
|
||||
),
|
||||
);
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for figuring out if the entities with the given entity info
|
||||
* can be filtered by bundle.
|
||||
*/
|
||||
protected static function hasBundles(array $entity_info) {
|
||||
return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
|
||||
}
|
||||
|
||||
}
|
155
includes/callback_language_control.inc
Normal file
155
includes/callback_language_control.inc
Normal file
@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Search API data alteration callback that filters out items based on their
|
||||
* bundle.
|
||||
*/
|
||||
class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Construct a data-alter callback.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose items will be altered.
|
||||
* @param array $options
|
||||
* The callback options set for this index.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
$options += array(
|
||||
'lang_field' => '',
|
||||
'languages' => array(),
|
||||
);
|
||||
parent::__construct($index, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* Only returns TRUE if the system is multilingual.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*
|
||||
* @see drupal_multilingual()
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return drupal_multilingual();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a form for configuring this data alteration.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this data alteration.
|
||||
*/
|
||||
public function configurationForm() {
|
||||
$form = array();
|
||||
|
||||
$wrapper = $this->index->entityWrapper();
|
||||
$fields[''] = t('- Use default -');
|
||||
foreach ($wrapper as $key => $property) {
|
||||
if ($key == 'search_api_language') {
|
||||
continue;
|
||||
}
|
||||
$type = $property->type();
|
||||
// Only single-valued string properties make sense here. Also, nested
|
||||
// properties probably don't make sense.
|
||||
if ($type == 'text' || $type == 'token') {
|
||||
$info = $property->info();
|
||||
$fields[$key] = $info['label'];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($fields) > 1) {
|
||||
$form['lang_field'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Language field'),
|
||||
'#description' => t("Select the field which should be used to determine an item's language."),
|
||||
'#options' => $fields,
|
||||
'#default_value' => $this->options['lang_field'],
|
||||
);
|
||||
}
|
||||
|
||||
$languages[LANGUAGE_NONE] = t('Language neutral');
|
||||
$list = language_list('enabled') + array(array(), array());
|
||||
foreach (array($list[1], $list[0]) as $list) {
|
||||
foreach ($list as $lang) {
|
||||
$name = t($lang->name);
|
||||
$native = $lang->native;
|
||||
$languages[$lang->language] = ($name == $native) ? $name : "$name ($native)";
|
||||
if (!$lang->enabled) {
|
||||
$languages[$lang->language] .= ' [' . t('disabled') . ']';
|
||||
}
|
||||
}
|
||||
}
|
||||
$form['languages'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Indexed languages'),
|
||||
'#description' => t('Index only items in the selected languages. ' .
|
||||
'When no language is selected, there will be no language-related restrictions.'),
|
||||
'#options' => $languages,
|
||||
'#default_value' => $this->options['languages'],
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$values['languages'] = array_filter($values['languages']);
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing. This could for instance be used to implement
|
||||
* some sort of access filter for security purposes (e.g., don't index
|
||||
* unpublished nodes or comments).
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
foreach ($items as $i => &$item) {
|
||||
// Set item language, if a custom field was selected.
|
||||
if ($field = $this->options['lang_field']) {
|
||||
$wrapper = $this->index->entityWrapper($item);
|
||||
if (isset($wrapper->$field)) {
|
||||
try {
|
||||
$item->search_api_language = $wrapper->$field->value();
|
||||
}
|
||||
catch (EntityMetadataWrapperException $e) {
|
||||
// Something went wrong while accessing the language field. Probably
|
||||
// doesn't really matter.
|
||||
}
|
||||
}
|
||||
}
|
||||
// Filter out items according to language, if any were selected.
|
||||
if ($languages = $this->options['languages']) {
|
||||
if (empty($languages[$item->search_api_language])) {
|
||||
unset($items[$i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
109
includes/callback_node_access.inc
Normal file
109
includes/callback_node_access.inc
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterNodeAccess class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds node access information to node indexes.
|
||||
*/
|
||||
class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* Returns TRUE only for indexes on nodes.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
// Currently only node access is supported.
|
||||
return $index->item_type === 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare the properties that are (or can be) added to items with this callback.
|
||||
*
|
||||
* Adds the "search_api_access_node" property.
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*
|
||||
* @return array
|
||||
* Information about all additional properties, as specified by
|
||||
* hook_entity_property_info() (only the inner "properties" array).
|
||||
*/
|
||||
public function propertyInfo() {
|
||||
return array(
|
||||
'search_api_access_node' => array(
|
||||
'label' => t('Node access information'),
|
||||
'description' => t('Data needed to apply node access.'),
|
||||
'type' => 'list<token>',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing. This could for instance be used to implement
|
||||
* some sort of access filter for security purposes (e.g., don't index
|
||||
* unpublished nodes or comments).
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
static $account;
|
||||
|
||||
if (!isset($account)) {
|
||||
// Load the anonymous user.
|
||||
$account = drupal_anonymous_user();
|
||||
}
|
||||
|
||||
foreach ($items as $nid => &$item) {
|
||||
// Check whether all users have access to the node.
|
||||
if (!node_access('view', $item, $account)) {
|
||||
// Get node access grants.
|
||||
$result = db_query('SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1', array(':nid' => $item->nid));
|
||||
|
||||
// Store all grants together with it's realms in the item.
|
||||
foreach ($result as $grant) {
|
||||
if (!isset($items[$nid]->search_api_access_node)) {
|
||||
$items[$nid]->search_api_access_node = array();
|
||||
}
|
||||
$items[$nid]->search_api_access_node[] = "node_access_$grant->realm:$grant->gid";
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Add the generic view grant if we are not using node access or the
|
||||
// node is viewable by anonymous users.
|
||||
$items[$nid]->search_api_access_node = array('node_access__all');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the configuration form.
|
||||
*
|
||||
* If the data alteration is being enabled, set "Published" and "Author" to
|
||||
* "indexed", because both are needed for the node access filter.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_node_access']['status']);
|
||||
$new_status = !empty($form_state['values']['callbacks']['search_api_alter_node_access']['status']);
|
||||
|
||||
if (!$old_status && $new_status) {
|
||||
$form_state['index']->options['fields']['status']['type'] = 'boolean';
|
||||
$form_state['index']->options['fields']['author']['type'] = 'integer';
|
||||
$form_state['index']->options['fields']['author']['entity_type'] = 'user';
|
||||
}
|
||||
|
||||
return parent::configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
}
|
45
includes/callback_node_status.inc
Normal file
45
includes/callback_node_status.inc
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiAlterNodeStatus class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exclude unpublished nodes from node indexes.
|
||||
*/
|
||||
class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback {
|
||||
|
||||
/**
|
||||
* Check whether this data-alter callback is applicable for a certain index.
|
||||
*
|
||||
* Returns TRUE only for indexes on nodes.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the callback can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return $index->item_type === 'node';
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter items before indexing.
|
||||
*
|
||||
* Items which are removed from the array won't be indexed, but will be marked
|
||||
* as clean for future indexing.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered, keyed by item IDs.
|
||||
*/
|
||||
public function alterItems(array &$items) {
|
||||
foreach ($items as $nid => &$item) {
|
||||
if (empty($item->status)) {
|
||||
unset($items[$nid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
698
includes/datasource.inc
Executable file
698
includes/datasource.inc
Executable file
@ -0,0 +1,698 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiDataSourceControllerInterface as well as a default base class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for all data source controllers for Search API indexes.
|
||||
*
|
||||
* Data source controllers encapsulate all operations specific to an item type.
|
||||
* They are used for loading items, extracting item data, keeping track of the
|
||||
* item status, etc.
|
||||
*
|
||||
* All methods of the data source may throw exceptions of type
|
||||
* SearchApiDataSourceException if any exception or error state is encountered.
|
||||
*/
|
||||
interface SearchApiDataSourceControllerInterface {
|
||||
|
||||
/**
|
||||
* Constructor for a data source controller.
|
||||
*
|
||||
* @param $type
|
||||
* The item type for which this controller is created.
|
||||
*/
|
||||
public function __construct($type);
|
||||
|
||||
/**
|
||||
* Return information on the ID field for this controller's type.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the following keys:
|
||||
* - key: The property key for the ID field, as used in the item wrapper.
|
||||
* - type: The type of the ID field. Has to be one of the types from
|
||||
* search_api_field_types(). List types ("list<*>") are not allowed.
|
||||
*/
|
||||
public function getIdFieldInfo();
|
||||
|
||||
/**
|
||||
* Load items of the type of this data source controller.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to laod.
|
||||
*
|
||||
* @return array
|
||||
* The loaded items, keyed by ID.
|
||||
*/
|
||||
public function loadItems(array $ids);
|
||||
|
||||
/**
|
||||
* Get a metadata wrapper for the item type of this data source controller.
|
||||
*
|
||||
* @param $item
|
||||
* Unless NULL, an item of the item type for this controller to be wrapped.
|
||||
* @param array $info
|
||||
* Optionally, additional information that should be used for creating the
|
||||
* wrapper. Uses the same format as entity_metadata_wrapper().
|
||||
*
|
||||
* @return EntityMetadataWrapper
|
||||
* A wrapper for the item type of this data source controller, according to
|
||||
* the info array, and optionally loaded with the given data.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array());
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either the unique ID of the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemId($item);
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either a human-readable label for the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemLabel($item);
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either an array containing the 'path' and 'options' keys used to build
|
||||
* the URL of the item, and matching the signature of url(), or NULL if the
|
||||
* item has no URL of its own.
|
||||
*/
|
||||
public function getItemUrl($item);
|
||||
|
||||
/**
|
||||
* Initialize tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* All currently known items of this data source's type should be inserted
|
||||
* into the tracking table for the given indexes, with status "changed". If
|
||||
* items were already present, these should also be set to "changed" and not
|
||||
* be inserted again.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be initialized.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function startTracking(array $indexes);
|
||||
|
||||
/**
|
||||
* Stop tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* The tracking tables of the given indexes should be completely cleared.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be stopped.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function stopTracking(array $indexes);
|
||||
|
||||
/**
|
||||
* Start tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of new items to track.
|
||||
* @param array $indexes
|
||||
* The indexes for which items should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemInsert(array $item_ids, array $indexes);
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "changed"/"dirty".
|
||||
*
|
||||
* Unless $dequeue is set to TRUE, this operation is ignored for items whose
|
||||
* status is not "indexed".
|
||||
*
|
||||
* @param $item_ids
|
||||
* Either an array with the IDs of the changed items. Or FALSE to mark all
|
||||
* items as changed for the given indexes.
|
||||
* @param array $indexes
|
||||
* The indexes for which the change should be tracked.
|
||||
* @param $dequeue
|
||||
* If set to TRUE, also change the status of queued items.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE);
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "queued".
|
||||
*
|
||||
* Queued items are not marked as "dirty" even when they are changed, and they
|
||||
* are not returned by the getChangedItems() method.
|
||||
*
|
||||
* @param $item_ids
|
||||
* Either an array with the IDs of the queued items. Or FALSE to mark all
|
||||
* items as queued for the given indexes.
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which the items were queued.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemQueued($item_ids, SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "indexed".
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the indexed items.
|
||||
* @param SearchApiIndex $indexes
|
||||
* The index on which the items were indexed.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemIndexed(array $item_ids, SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Stop tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the removed items.
|
||||
* @param array $indexes
|
||||
* The indexes for which the deletions should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemDelete(array $item_ids, array $indexes);
|
||||
|
||||
/**
|
||||
* Get a list of items that need to be indexed.
|
||||
*
|
||||
* If possible, completely unindexed items should be returned before items
|
||||
* that were indexed but later changed. Also, items that were changed longer
|
||||
* ago should be favored.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which changed items should be returned.
|
||||
* @param $limit
|
||||
* The maximum number of items to return. Negative values mean "unlimited".
|
||||
*
|
||||
* @return array
|
||||
* The IDs of items that need to be indexed for the given index.
|
||||
*/
|
||||
public function getChangedItems(SearchApiIndex $index, $limit = -1);
|
||||
|
||||
/**
|
||||
* Get information on how many items have been indexed for a certain index.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose index status should be returned.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing two keys (in this order):
|
||||
* - indexed: The number of items already indexed in their latest version.
|
||||
* - total: The total number of items that have to be indexed for this
|
||||
* index.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function getIndexStatus(SearchApiIndex $index);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default base class for the SearchApiDataSourceControllerInterface.
|
||||
*
|
||||
* Contains default implementations for a number of methods which will be
|
||||
* similar for most data sources. Concrete data sources can decide to extend
|
||||
* this base class to save time, but can also implement the interface directly.
|
||||
*
|
||||
* A subclass will still have to provide implementations for the following
|
||||
* methods:
|
||||
* - getIdFieldInfo()
|
||||
* - loadItems()
|
||||
* - getMetadataWrapper() or getPropertyInfo()
|
||||
* - startTracking() or getAllItemIds()
|
||||
*
|
||||
* The table used by default for tracking the index status of items is
|
||||
* {search_api_item}. This can easily be changed, for example when an item type
|
||||
* has non-integer IDs, by changing the $table property.
|
||||
*/
|
||||
abstract class SearchApiAbstractDataSourceController implements SearchApiDataSourceControllerInterface {
|
||||
|
||||
/**
|
||||
* The item type for this controller instance.
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* The info array for the item type, as specified via
|
||||
* hook_search_api_item_type_info().
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $info;
|
||||
|
||||
/**
|
||||
* The table used for tracking items. Set to NULL on subclasses to disable
|
||||
* the default tracking for an item type, or change the property to use a
|
||||
* different table for tracking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'search_api_item';
|
||||
|
||||
/**
|
||||
* When using the default tracking mechanism: the name of the column on
|
||||
* $this->table containing the item ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $itemIdColumn = 'item_id';
|
||||
|
||||
/**
|
||||
* When using the default tracking mechanism: the name of the column on
|
||||
* $this->table containing the index ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $indexIdColumn = 'index_id';
|
||||
|
||||
/**
|
||||
* When using the default tracking mechanism: the name of the column on
|
||||
* $this->table containing the indexing status.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $changedColumn = 'changed';
|
||||
|
||||
/**
|
||||
* Constructor for a data source controller.
|
||||
*
|
||||
* @param $type
|
||||
* The item type for which this controller is created.
|
||||
*/
|
||||
public function __construct($type) {
|
||||
$this->type = $type;
|
||||
$this->info = search_api_get_item_type_info($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metadata wrapper for the item type of this data source controller.
|
||||
*
|
||||
* @param $item
|
||||
* Unless NULL, an item of the item type for this controller to be wrapped.
|
||||
* @param array $info
|
||||
* Optionally, additional information that should be used for creating the
|
||||
* wrapper. Uses the same format as entity_metadata_wrapper().
|
||||
*
|
||||
* @return EntityMetadataWrapper
|
||||
* A wrapper for the item type of this data source controller, according to
|
||||
* the info array, and optionally loaded with the given data.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array()) {
|
||||
$info += $this->getPropertyInfo();
|
||||
return entity_metadata_wrapper($this->type, $item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses to specify the property
|
||||
* information to use when creating a metadata wrapper.
|
||||
*
|
||||
* @return array
|
||||
* Property information as specified by hook_entity_property_info().
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*/
|
||||
protected function getPropertyInfo() {
|
||||
throw new SearchApiDataSourceException(t('No known property information for type @type.', array('@type' => $this->type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either the unique ID of the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemId($item) {
|
||||
$id_info = $this->getIdFieldInfo();
|
||||
$field = $id_info['key'];
|
||||
$wrapper = $this->getMetadataWrapper($item);
|
||||
if (!isset($wrapper->$field)) {
|
||||
return NULL;
|
||||
}
|
||||
$id = $wrapper->$field->value();
|
||||
return $id ? $id : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either a human-readable label for the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemLabel($item) {
|
||||
$label = $this->getMetadataWrapper($item)->label();
|
||||
return $label ? $label : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either an array containing the 'path' and 'options' keys used to build
|
||||
* the URL of the item, and matching the signature of url(), or NULL if the
|
||||
* item has no URL of its own.
|
||||
*/
|
||||
public function getItemUrl($item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* All currently known items of this data source's type should be inserted
|
||||
* into the tracking table for the given indexes, with status "changed". If
|
||||
* items were already present, these should also be set to "changed" and not
|
||||
* be inserted again.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be initialized.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function startTracking(array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
// We first clear the tracking table for all indexes, so we can just insert
|
||||
// all items again without any key conflicts.
|
||||
$this->stopTracking($indexes);
|
||||
// Insert all items as new.
|
||||
$this->trackItemInsert($this->getAllItemIds(), $indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses instead of implementing startTracking().
|
||||
*
|
||||
* Returns the IDs of all items that are known for this controller's type.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all item IDs for this type.
|
||||
*/
|
||||
protected function getAllItemIds() {
|
||||
throw new SearchApiDataSourceException(t('Items not known for type @type.', array('@type' => $this->type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* The tracking tables of the given indexes should be completely cleared.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be stopped.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function stopTracking(array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
// We could also use a single query with "IN" operator, but this method
|
||||
// will mostly be called with only one index.
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$query = db_delete($this->table)
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of new items to track.
|
||||
* @param array $indexes
|
||||
* The indexes for which items should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemInsert(array $item_ids, array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since large amounts of items can overstrain the database, only add items
|
||||
// in chunks.
|
||||
foreach (array_chunk($item_ids, 1000) as $chunk) {
|
||||
$insert = db_insert($this->table)
|
||||
->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn));
|
||||
foreach ($chunk as $item_id) {
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$insert->values(array(
|
||||
$this->itemIdColumn => $item_id,
|
||||
$this->indexIdColumn => $index->id,
|
||||
$this->changedColumn => 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
$insert->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "changed"/"dirty".
|
||||
*
|
||||
* Unless $dequeue is set to TRUE, this operation is ignored for items whose
|
||||
* status is not "indexed".
|
||||
*
|
||||
* @param $item_ids
|
||||
* Either an array with the IDs of the changed items. Or FALSE to mark all
|
||||
* items as changed for the given indexes.
|
||||
* @param array $indexes
|
||||
* The indexes for which the change should be tracked.
|
||||
* @param $dequeue
|
||||
* If set to TRUE, also change the status of queued items.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$index_ids = array();
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$index_ids[] = $index->id;
|
||||
}
|
||||
$update = db_update($this->table)
|
||||
->fields(array(
|
||||
$this->changedColumn => REQUEST_TIME,
|
||||
))
|
||||
->condition($this->indexIdColumn, $index_ids, 'IN')
|
||||
->condition($this->changedColumn, 0, $dequeue ? '<=' : '=');
|
||||
if ($item_ids !== FALSE) {
|
||||
$update->condition($this->itemIdColumn, $item_ids, 'IN');
|
||||
}
|
||||
$update->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "queued".
|
||||
*
|
||||
* Queued items are not marked as "dirty" even when they are changed, and they
|
||||
* are not returned by the getChangedItems() method.
|
||||
*
|
||||
* @param $item_ids
|
||||
* Either an array with the IDs of the queued items. Or FALSE to mark all
|
||||
* items as queued for the given indexes.
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which the items were queued.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemQueued($item_ids, SearchApiIndex $index) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$update = db_update($this->table)
|
||||
->fields(array(
|
||||
$this->changedColumn => -1,
|
||||
))
|
||||
->condition($this->indexIdColumn, $index->id);
|
||||
if ($item_ids !== FALSE) {
|
||||
$update->condition($this->itemIdColumn, $item_ids, 'IN');
|
||||
}
|
||||
$update->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "indexed".
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the indexed items.
|
||||
* @param SearchApiIndex $indexes
|
||||
* The index on which the items were indexed.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$this->checkIndex($index);
|
||||
db_update($this->table)
|
||||
->fields(array(
|
||||
$this->changedColumn => 0,
|
||||
))
|
||||
->condition($this->itemIdColumn, $item_ids, 'IN')
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the removed items.
|
||||
* @param array $indexes
|
||||
* The indexes for which the deletions should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemDelete(array $item_ids, array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
$index_ids = array();
|
||||
foreach ($indexes as $index) {
|
||||
$this->checkIndex($index);
|
||||
$index_ids[] = $index->id;
|
||||
}
|
||||
db_delete($this->table)
|
||||
->condition($this->itemIdColumn, $item_ids, 'IN')
|
||||
->condition($this->indexIdColumn, $index_ids, 'IN')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of items that need to be indexed.
|
||||
*
|
||||
* If possible, completely unindexed items should be returned before items
|
||||
* that were indexed but later changed. Also, items that were changed longer
|
||||
* ago should be favored.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which changed items should be returned.
|
||||
* @param $limit
|
||||
* The maximum number of items to return. Negative values mean "unlimited".
|
||||
*
|
||||
* @return array
|
||||
* The IDs of items that need to be indexed for the given index.
|
||||
*/
|
||||
public function getChangedItems(SearchApiIndex $index, $limit = -1) {
|
||||
if ($limit == 0) {
|
||||
return array();
|
||||
}
|
||||
$this->checkIndex($index);
|
||||
$select = db_select($this->table, 't');
|
||||
$select->addField('t', 'item_id');
|
||||
$select->condition($this->indexIdColumn, $index->id);
|
||||
$select->condition($this->changedColumn, 0, '>');
|
||||
$select->orderBy($this->changedColumn, 'ASC');
|
||||
if ($limit > 0) {
|
||||
$select->range(0, $limit);
|
||||
}
|
||||
return $select->execute()->fetchCol();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information on how many items have been indexed for a certain index.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose index status should be returned.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing two keys (in this order):
|
||||
* - indexed: The number of items already indexed in their latest version.
|
||||
* - total: The total number of items that have to be indexed for this
|
||||
* index.
|
||||
*/
|
||||
public function getIndexStatus(SearchApiIndex $index) {
|
||||
if (!$this->table) {
|
||||
return array('indexed' => 0, 'total' => 0);
|
||||
}
|
||||
$this->checkIndex($index);
|
||||
$indexed = db_select($this->table, 'i')
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->condition($this->changedColumn, 0)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
$total = db_select($this->table, 'i')
|
||||
->condition($this->indexIdColumn, $index->id)
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
return array('indexed' => $indexed, 'total' => $total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for ensuring that an index uses the same item type as this controller.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same type as this controller.
|
||||
*/
|
||||
protected function checkIndex(SearchApiIndex $index) {
|
||||
if ($index->item_type != $this->type) {
|
||||
$index_type = search_api_get_item_type_info($index->item_type);
|
||||
$index_type = empty($index_type['name']) ? $index->item_type : $index_type['name'];
|
||||
$msg = t('Invalid index @index of type @index_type passed to data source controller for type @this_type.',
|
||||
array('@index' => $index->name, '@index_type' => $index_type, '@this_type' => $this->info['name']));
|
||||
throw new SearchApiDataSourceException($msg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
206
includes/datasource_entity.inc
Normal file
206
includes/datasource_entity.inc
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiEntityDataSourceController class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data source for all entities known to the Entity API.
|
||||
*/
|
||||
class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController {
|
||||
|
||||
/**
|
||||
* Return information on the ID field for this controller's type.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the following keys:
|
||||
* - key: The property key for the ID field, as used in the item wrapper.
|
||||
* - type: The type of the ID field. Has to be one of the types from
|
||||
* search_api_field_types(). List types ("list<*>") are not allowed.
|
||||
*/
|
||||
public function getIdFieldInfo() {
|
||||
$info = entity_get_info($this->type);
|
||||
$properties = entity_get_property_info($this->type);
|
||||
if (empty($info['entity keys']['id'])) {
|
||||
throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label'])));
|
||||
}
|
||||
$field = $info['entity keys']['id'];
|
||||
if (empty($properties['properties'][$field]['type'])) {
|
||||
throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $info['label'], '@prop' => $field)));
|
||||
}
|
||||
$type = $properties['properties'][$field]['type'];
|
||||
if (search_api_is_list_type($type)) {
|
||||
throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $info['label'], '@prop' => $field)));
|
||||
}
|
||||
if ($type == 'token') {
|
||||
$type = 'string';
|
||||
}
|
||||
return array(
|
||||
'key' => $field,
|
||||
'type' => $type,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load items of the type of this data source controller.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to laod.
|
||||
*
|
||||
* @return array
|
||||
* The loaded items, keyed by ID.
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
$items = entity_load($this->type, $ids);
|
||||
// If some items couldn't be loaded, remove them from tracking.
|
||||
if (count($items) != count($ids)) {
|
||||
$ids = array_flip($ids);
|
||||
$unknown = array_keys(array_diff_key($ids, $items));
|
||||
if ($unknown) {
|
||||
search_api_track_item_delete($this->type, $unknown);
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metadata wrapper for the item type of this data source controller.
|
||||
*
|
||||
* @param $item
|
||||
* Unless NULL, an item of the item type for this controller to be wrapped.
|
||||
* @param array $info
|
||||
* Optionally, additional information that should be used for creating the
|
||||
* wrapper. Uses the same format as entity_metadata_wrapper().
|
||||
*
|
||||
* @return EntityMetadataWrapper
|
||||
* A wrapper for the item type of this data source controller, according to
|
||||
* the info array, and optionally loaded with the given data.
|
||||
*
|
||||
* @see entity_metadata_wrapper()
|
||||
*/
|
||||
public function getMetadataWrapper($item = NULL, array $info = array()) {
|
||||
return entity_metadata_wrapper($this->type, $item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either the unique ID of the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemId($item) {
|
||||
$id = entity_id($this->type, $item);
|
||||
return $id ? $id : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either a human-readable label for the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemLabel($item) {
|
||||
$label = entity_label($this->type, $item);
|
||||
return $label ? $label : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either an array containing the 'path' and 'options' keys used to build
|
||||
* the URL of the item, and matching the signature of url(), or NULL if the
|
||||
* item has no URL of its own.
|
||||
*/
|
||||
public function getItemUrl($item) {
|
||||
if ($this->type == 'file') {
|
||||
return array(
|
||||
'path' => file_create_url($item->uri),
|
||||
'options' => array(
|
||||
'entity_type' => 'file',
|
||||
'entity' => $item,
|
||||
),
|
||||
);
|
||||
}
|
||||
$url = entity_uri($this->type, $item);
|
||||
return $url ? $url : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* All currently known items of this data source's type should be inserted
|
||||
* into the tracking table for the given indexes, with status "changed". If
|
||||
* items were already present, these should also be set to "changed" and not
|
||||
* be inserted again.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be initialized.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function startTracking(array $indexes) {
|
||||
if (!$this->table) {
|
||||
return;
|
||||
}
|
||||
// We first clear the tracking table for all indexes, so we can just insert
|
||||
// all items again without any key conflicts.
|
||||
$this->stopTracking($indexes);
|
||||
|
||||
$entity_info = entity_get_info($this->type);
|
||||
|
||||
if (!empty($entity_info['base table'])) {
|
||||
// Use a subselect, which will probably be much faster than entity_load().
|
||||
|
||||
// Assumes that all entities use the "base table" property and the
|
||||
// "entity keys[id]" in the same way as the default controller.
|
||||
$id_field = $entity_info['entity keys']['id'];
|
||||
$table = $entity_info['base table'];
|
||||
|
||||
// We could also use a single insert (with a JOIN in the nested query),
|
||||
// but this method will be mostly called with a single index, anyways.
|
||||
foreach ($indexes as $index) {
|
||||
// Select all entity ids.
|
||||
$query = db_select($table, 't');
|
||||
$query->addField('t', $id_field, 'item_id');
|
||||
$query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id));
|
||||
$query->addExpression('1', 'changed');
|
||||
|
||||
// INSERT ... SELECT ...
|
||||
db_insert($this->table)
|
||||
->from($query)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// In the absence of a 'base table', use the slow entity_load().
|
||||
parent::startTracking($indexes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses instead of implementing startTracking().
|
||||
*
|
||||
* Returns the IDs of all items that are known for this controller's type.
|
||||
*
|
||||
* Will be used when the entity type doesn't specify a "base table".
|
||||
*
|
||||
* @return array
|
||||
* An array containing all item IDs for this type.
|
||||
*/
|
||||
protected function getAllItemIds() {
|
||||
return array_keys(entity_load($this->type));
|
||||
}
|
||||
|
||||
}
|
268
includes/datasource_external.inc
Normal file
268
includes/datasource_external.inc
Normal file
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains the SearchApiExternalDataSourceController class.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for data source controllers for external data sources.
|
||||
*
|
||||
* This data source controller is a base implementation for item types that
|
||||
* represent external data, not directly accessible in Drupal. You can use this
|
||||
* controller as a base class when you don't want to index items of the type via
|
||||
* Drupal, but only want the search capabilities of the Search API. In addition
|
||||
* you most probably also have to create a fitting service class for executing
|
||||
* the actual searches.
|
||||
*
|
||||
* To use most of the functionality of the Search API and related modules, you
|
||||
* will only have to specify some property information in getPropertyInfo(). If
|
||||
* you have a custom service class which already returns the extracted fields
|
||||
* with the search results, you will only have to provide a label and a type for
|
||||
* each field. To make this use case easier, there is also a
|
||||
* getFieldInformation() method which you can implement instead of directly
|
||||
* implementing getPropertyInfo().
|
||||
*/
|
||||
class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceController {
|
||||
|
||||
/**
|
||||
* Return information on the ID field for this controller's type.
|
||||
*
|
||||
* This implementation will return a field named "id" of type "string". This
|
||||
* can also be used if the item type in question has no IDs.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the following keys:
|
||||
* - key: The property key for the ID field, as used in the item wrapper.
|
||||
* - type: The type of the ID field. Has to be one of the types from
|
||||
* search_api_field_types(). List types ("list<*>") are not allowed.
|
||||
*/
|
||||
public function getIdFieldInfo() {
|
||||
return array(
|
||||
'key' => 'id',
|
||||
'type' => 'string',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load items of the type of this data source controller.
|
||||
*
|
||||
* Always returns an empty array. If you want the items of your type to be
|
||||
* loadable, specify a function here.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to laod.
|
||||
*
|
||||
* @return array
|
||||
* The loaded items, keyed by ID.
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that can be used by subclasses to specify the property
|
||||
* information to use when creating a metadata wrapper.
|
||||
*
|
||||
* For most use cases, you will have to override this method to provide the
|
||||
* real property information for your item type.
|
||||
*
|
||||
* @return array
|
||||
* Property information as specified by hook_entity_property_info().
|
||||
*
|
||||
* @see hook_entity_property_info()
|
||||
*/
|
||||
protected function getPropertyInfo() {
|
||||
$info['property info']['id'] = array(
|
||||
'label' => t('ID'),
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique ID of an item.
|
||||
*
|
||||
* Always returns 1.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either the unique ID of the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemId($item) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for an item.
|
||||
*
|
||||
* Always returns NULL.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either a human-readable label for the item, or NULL if none is available.
|
||||
*/
|
||||
public function getItemLabel($item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL at which the item can be viewed on the web.
|
||||
*
|
||||
* Always returns NULL.
|
||||
*
|
||||
* @param $item
|
||||
* An item of this controller's type.
|
||||
*
|
||||
* @return
|
||||
* Either an array containing the 'path' and 'options' keys used to build
|
||||
* the URL of the item, and matching the signature of url(), or NULL if the
|
||||
* item has no URL of its own.
|
||||
*/
|
||||
public function getItemUrl($item) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* All currently known items of this data source's type should be inserted
|
||||
* into the tracking table for the given indexes, with status "changed". If
|
||||
* items were already present, these should also be set to "changed" and not
|
||||
* be inserted again.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be initialized.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function startTracking(array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking of the index status of items for the given indexes.
|
||||
*
|
||||
* The tracking tables of the given indexes should be completely cleared.
|
||||
*
|
||||
* @param array $indexes
|
||||
* The SearchApiIndex objects for which item tracking should be stopped.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function stopTracking(array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of new items to track.
|
||||
* @param array $indexes
|
||||
* The indexes for which items should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemInsert(array $item_ids, array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "changed"/"dirty".
|
||||
*
|
||||
* @param $item_ids
|
||||
* Either an array with the IDs of the changed items. Or FALSE to mark all
|
||||
* items as changed for the given indexes.
|
||||
* @param array $indexes
|
||||
* The indexes for which the change should be tracked.
|
||||
* @param $dequeue
|
||||
* If set to TRUE, also change the status of queued items.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking status of the given items to "indexed".
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the indexed items.
|
||||
* @param SearchApiIndex $indexes
|
||||
* The index on which the items were indexed.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking the index status for the given items on the given indexes.
|
||||
*
|
||||
* @param array $item_ids
|
||||
* The IDs of the removed items.
|
||||
* @param array $indexes
|
||||
* The indexes for which the deletions should be tracked.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If any of the indexes doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function trackItemDelete(array $item_ids, array $indexes) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of items that need to be indexed.
|
||||
*
|
||||
* If possible, completely unindexed items should be returned before items
|
||||
* that were indexed but later changed. Also, items that were changed longer
|
||||
* ago should be favored.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which changed items should be returned.
|
||||
* @param $limit
|
||||
* The maximum number of items to return. Negative values mean "unlimited".
|
||||
*
|
||||
* @return array
|
||||
* The IDs of items that need to be indexed for the given index.
|
||||
*/
|
||||
public function getChangedItems(SearchApiIndex $index, $limit = -1) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information on how many items have been indexed for a certain index.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index whose index status should be returned.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing two keys (in this order):
|
||||
* - indexed: The number of items already indexed in their latest version.
|
||||
* - total: The total number of items that have to be indexed for this
|
||||
* index.
|
||||
*
|
||||
* @throws SearchApiDataSourceException
|
||||
* If the index doesn't use the same item type as this controller.
|
||||
*/
|
||||
public function getIndexStatus(SearchApiIndex $index) {
|
||||
return array(
|
||||
'indexed' => 0,
|
||||
'total' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
29
includes/exception.inc
Normal file
29
includes/exception.inc
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents an exception or error that occurred in some part of the Search API
|
||||
* framework.
|
||||
*/
|
||||
class SearchApiException extends Exception {
|
||||
|
||||
/**
|
||||
* Creates a new SearchApiException.
|
||||
*
|
||||
* @param $message
|
||||
* A string describing the cause of the exception.
|
||||
*/
|
||||
public function __construct($message = NULL) {
|
||||
if (!$message) {
|
||||
$message = t('An error occcurred in the Search API framework.');
|
||||
}
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an exception that occurred in a data source controller.
|
||||
*/
|
||||
class SearchApiDataSourceException extends SearchApiException {
|
||||
|
||||
}
|
936
includes/index_entity.inc
Normal file
936
includes/index_entity.inc
Normal file
@ -0,0 +1,936 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class representing a search index.
|
||||
*/
|
||||
class SearchApiIndex extends Entity {
|
||||
|
||||
// Cache values, set when the corresponding methods are called for the first
|
||||
// time.
|
||||
|
||||
/**
|
||||
* Cached return value of datasource().
|
||||
*
|
||||
* @var SearchApiDataSourceControllerInterface
|
||||
*/
|
||||
protected $datasource = NULL;
|
||||
|
||||
/**
|
||||
* Cached return value of server().
|
||||
*
|
||||
* @var SearchApiServer
|
||||
*/
|
||||
protected $server_object = NULL;
|
||||
|
||||
/**
|
||||
* All enabled data alterations for this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $callbacks = NULL;
|
||||
|
||||
/**
|
||||
* All enabled processors for this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $processors = NULL;
|
||||
|
||||
/**
|
||||
* The properties added by data alterations on this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $added_properties = NULL;
|
||||
|
||||
/**
|
||||
* An array containing two arrays.
|
||||
*
|
||||
* At index 0, all fulltext fields of this index. At index 1, all indexed
|
||||
* fulltext fields of this index.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fulltext_fields = array();
|
||||
|
||||
// Database values that will be set when object is loaded.
|
||||
|
||||
/**
|
||||
* An integer identifying the index.
|
||||
* Immutable.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* A name to be displayed for the index.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The machine name of the index.
|
||||
* Immutable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $machine_name;
|
||||
|
||||
/**
|
||||
* A string describing the index' use to users.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* The machine_name of the server with which data should be indexed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $server;
|
||||
|
||||
/**
|
||||
* The type of items stored in this index.
|
||||
* Immutable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $item_type;
|
||||
|
||||
/**
|
||||
* An array of options for configuring this index. The layout is as follows:
|
||||
* - cron_limit: The maximum number of items to be indexed per cron batch.
|
||||
* - index_directly: Boolean setting whether entities are indexed immediately
|
||||
* after they are created or updated.
|
||||
* - fields: An array of all indexed fields for this index. Keys are the field
|
||||
* identifiers, the values are arrays for specifying the field settings. The
|
||||
* structure of those arrays looks like this:
|
||||
* - type: The type set for this field. One of the types returned by
|
||||
* search_api_default_field_types().
|
||||
* - real_type: (optional) If a custom data type was selected for this
|
||||
* field, this type will be stored here, and "type" contain the fallback
|
||||
* default data type.
|
||||
* - boost: (optional) A boost value for terms found in this field during
|
||||
* searches. Usually only relevant for fulltext fields. Defaults to 1.0.
|
||||
* - entity_type (optional): If set, the type of this field is really an
|
||||
* entity. The "type" key will then just contain the primitive data type
|
||||
* of the ID field, meaning that servers will ignore this and merely index
|
||||
* the entity's ID. Components displaying this field, though, are advised
|
||||
* to use the entity label instead of the ID.
|
||||
* - additional fields: An associative array with keys and values being the
|
||||
* field identifiers of related entities whose fields should be displayed.
|
||||
* - data_alter_callbacks: An array of all data alterations available. Keys
|
||||
* are the alteration identifiers, the values are arrays containing the
|
||||
* settings for that data alteration. The inner structure looks like this:
|
||||
* - status: Boolean indicating whether the data alteration is enabled.
|
||||
* - weight: Used for sorting the data alterations.
|
||||
* - settings: Alteration-specific settings, configured via the alteration's
|
||||
* configuration form.
|
||||
* - processors: An array of all processors available for the index. The keys
|
||||
* are the processor identifiers, the values are arrays containing the
|
||||
* settings for that processor. The inner structure looks like this:
|
||||
* - status: Boolean indicating whether the processor is enabled.
|
||||
* - weight: Used for sorting the processors.
|
||||
* - settings: Processor-specific settings, configured via the processor's
|
||||
* configuration form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $options = array();
|
||||
|
||||
/**
|
||||
* A flag indicating whether this index is enabled.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $enabled = 1;
|
||||
|
||||
/**
|
||||
* A flag indicating whether to write to this index.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $read_only = 0;
|
||||
|
||||
/**
|
||||
* Constructor as a helper to the parent constructor.
|
||||
*/
|
||||
public function __construct(array $values = array()) {
|
||||
parent::__construct($values, 'search_api_index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute necessary tasks for a newly created index.
|
||||
*/
|
||||
public function postCreate() {
|
||||
if ($this->enabled) {
|
||||
$this->queueItems();
|
||||
}
|
||||
$server = $this->server();
|
||||
if ($server) {
|
||||
// Tell the server about the new index.
|
||||
if ($server->enabled) {
|
||||
$server->addIndex($this);
|
||||
}
|
||||
else {
|
||||
$tasks = variable_get('search_api_tasks', array());
|
||||
// When we add or remove an index, we can ignore all other tasks.
|
||||
$tasks[$server->machine_name][$this->machine_name] = array('add');
|
||||
variable_set('search_api_tasks', $tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute necessary tasks when the index is removed from the database.
|
||||
*/
|
||||
public function postDelete() {
|
||||
if ($server = $this->server()) {
|
||||
if ($server->enabled) {
|
||||
$server->removeIndex($this);
|
||||
}
|
||||
else {
|
||||
$tasks = variable_get('search_api_tasks', array());
|
||||
$tasks[$server->machine_name][$this->machine_name] = array('remove');
|
||||
variable_set('search_api_tasks', $tasks);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop tracking entities for indexing.
|
||||
$this->dequeueItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record entities to index.
|
||||
*/
|
||||
public function queueItems() {
|
||||
if (!$this->read_only) {
|
||||
$this->datasource()->startTracking(array($this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all records of entities to index.
|
||||
*/
|
||||
public function dequeueItems() {
|
||||
$this->datasource()->stopTracking(array($this));
|
||||
_search_api_empty_cron_queue($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this index to the database, either creating a new record or updating
|
||||
* an existing one.
|
||||
*
|
||||
* @return
|
||||
* Failure to save the index will return FALSE. Otherwise, SAVED_NEW or
|
||||
* SAVED_UPDATED is returned depending on the operation performed. $this->id
|
||||
* will be set if a new index was inserted.
|
||||
*/
|
||||
public function save() {
|
||||
if (empty($this->description)) {
|
||||
$this->description = NULL;
|
||||
}
|
||||
if (empty($this->server)) {
|
||||
$this->server = NULL;
|
||||
$this->enabled = FALSE;
|
||||
}
|
||||
// This will also throw an exception if the server doesn't exist – which is good.
|
||||
elseif (!$this->server(TRUE)->enabled) {
|
||||
$this->enabled = FALSE;
|
||||
}
|
||||
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for updating entity properties.
|
||||
*
|
||||
* NOTE: You shouldn't change any properties of this object before calling
|
||||
* this method, as this might lead to the fields not being saved correctly.
|
||||
*
|
||||
* @param array $fields
|
||||
* The new field values.
|
||||
*
|
||||
* @return
|
||||
* SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
|
||||
* the specified values.
|
||||
*/
|
||||
public function update(array $fields) {
|
||||
$changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'server' => 1, 'options' => 1, 'read_only' => 1);
|
||||
$changed = FALSE;
|
||||
foreach ($fields as $field => $value) {
|
||||
if (isset($changeable[$field]) && $value !== $this->$field) {
|
||||
$this->$field = $value;
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no new values, just return 0.
|
||||
if (!$changed) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reset the index's internal property cache to correctly incorporate new
|
||||
// settings.
|
||||
$this->resetCaches();
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules this search index for re-indexing.
|
||||
*
|
||||
* @return
|
||||
* TRUE on success, FALSE on failure.
|
||||
*/
|
||||
public function reindex() {
|
||||
if (!$this->server || $this->read_only) {
|
||||
return TRUE;
|
||||
}
|
||||
_search_api_index_reindex($this);
|
||||
module_invoke_all('search_api_index_reindex', $this, FALSE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears this search index and schedules all of its items for re-indexing.
|
||||
*
|
||||
* @return
|
||||
* TRUE on success, FALSE on failure.
|
||||
*/
|
||||
public function clear() {
|
||||
if (!$this->server || $this->read_only) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$server = $this->server();
|
||||
if ($server->enabled) {
|
||||
$server->deleteItems('all', $this);
|
||||
}
|
||||
else {
|
||||
$tasks = variable_get('search_api_tasks', array());
|
||||
// If the index was cleared or newly added since the server was last enabled, we don't need to do anything.
|
||||
if (!isset($tasks[$server->machine_name][$this->machine_name])
|
||||
|| (array_search('add', $tasks[$server->machine_name][$this->machine_name]) === FALSE
|
||||
&& array_search('clear', $tasks[$server->machine_name][$this->machine_name]) === FALSE)) {
|
||||
$tasks[$server->machine_name][$this->machine_name][] = 'clear';
|
||||
variable_set('search_api_tasks', $tasks);
|
||||
}
|
||||
}
|
||||
|
||||
_search_api_index_reindex($this);
|
||||
module_invoke_all('search_api_index_reindex', $this, TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method for determining which fields should be serialized.
|
||||
*
|
||||
* Don't serialize properties that are basically only caches.
|
||||
*
|
||||
* @return array
|
||||
* An array of properties to be serialized.
|
||||
*/
|
||||
public function __sleep() {
|
||||
$ret = get_object_vars($this);
|
||||
unset($ret['server_object'], $ret['datasource'], $ret['processors'], $ret['added_properties'], $ret['fulltext_fields']);
|
||||
return array_keys($ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the controller object of the data source used by this index.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the specified item type or data source doesn't exist or is invalid.
|
||||
*
|
||||
* @return SearchApiDataSourceControllerInterface
|
||||
* The data source controller for this index.
|
||||
*/
|
||||
public function datasource() {
|
||||
if (!isset($this->datasource)) {
|
||||
$this->datasource = search_api_get_datasource_controller($this->item_type);
|
||||
}
|
||||
return $this->datasource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server this index lies on.
|
||||
*
|
||||
* @param $reset
|
||||
* Whether to reset the internal cache. Set to TRUE when the index' $server
|
||||
* property has just changed.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If $this->server is set, but no server with that machine name exists.
|
||||
*
|
||||
* @return SearchApiServer
|
||||
* The server associated with this index, or NULL if this index currently
|
||||
* doesn't lie on a server.
|
||||
*/
|
||||
public function server($reset = FALSE) {
|
||||
if (!isset($this->server_object) || $reset) {
|
||||
$this->server_object = $this->server ? search_api_server_load($this->server) : FALSE;
|
||||
if ($this->server && !$this->server_object) {
|
||||
throw new SearchApiException(t('Unknown server @server specified for index @name.', array('@server' => $this->server, '@name' => $this->machine_name)));
|
||||
}
|
||||
}
|
||||
return $this->server_object ? $this->server_object : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query object for this index.
|
||||
*
|
||||
* @param $options
|
||||
* Associative array of options configuring this query. See
|
||||
* SearchApiQueryInterface::__construct().
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the index is currently disabled.
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* A query object for searching this index.
|
||||
*/
|
||||
public function query($options = array()) {
|
||||
if (!$this->enabled) {
|
||||
throw new SearchApiException(t('Cannot search on a disabled index.'));
|
||||
}
|
||||
return $this->server()->query($this, $options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indexes items on this index. Will return an array of IDs of items that
|
||||
* should be marked as indexed – i.e., items that were either rejected by a
|
||||
* data-alter callback or were successfully indexed.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to index.
|
||||
*
|
||||
* @return array
|
||||
* An array of the IDs of all items that should be marked as indexed.
|
||||
*/
|
||||
public function index(array $items) {
|
||||
if ($this->read_only) {
|
||||
return array();
|
||||
}
|
||||
if (!$this->enabled) {
|
||||
throw new SearchApiException(t("Couldn't index values on '@name' index (index is disabled)", array('@name' => $this->name)));
|
||||
}
|
||||
if (empty($this->options['fields'])) {
|
||||
throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
|
||||
}
|
||||
$fields = $this->options['fields'];
|
||||
$custom_type_fields = array();
|
||||
foreach ($fields as $field => $info) {
|
||||
if (isset($info['real_type'])) {
|
||||
$custom_type = search_api_extract_inner_type($info['real_type']);
|
||||
if ($this->server()->supportsFeature('search_api_data_type_' . $custom_type)) {
|
||||
$fields[$field]['type'] = $info['real_type'];
|
||||
$custom_type_fields[$custom_type][$field] = search_api_list_nesting_level($info['real_type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($fields)) {
|
||||
throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
|
||||
}
|
||||
|
||||
// Mark all items that are rejected as indexed.
|
||||
$ret = array_keys($items);
|
||||
drupal_alter('search_api_index_items', $items, $this);
|
||||
if ($items) {
|
||||
$this->dataAlter($items);
|
||||
}
|
||||
$ret = array_diff($ret, array_keys($items));
|
||||
|
||||
// Items that are rejected should also be deleted from the server.
|
||||
if ($ret) {
|
||||
$this->server()->deleteItems($ret, $this);
|
||||
}
|
||||
if (!$items) {
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$data = array();
|
||||
foreach ($items as $id => $item) {
|
||||
$data[$id] = search_api_extract_fields($this->entityWrapper($item), $fields);
|
||||
unset($items[$id]);
|
||||
foreach ($custom_type_fields as $type => $type_fields) {
|
||||
$info = search_api_get_data_type_info($type);
|
||||
if (isset($info['conversion callback']) && is_callable($info['conversion callback'])) {
|
||||
$callback = $info['conversion callback'];
|
||||
foreach ($type_fields as $field => $nesting_level) {
|
||||
if (isset($data[$id][$field]['value'])) {
|
||||
$value = $data[$id][$field]['value'];
|
||||
$original_type = $data[$id][$field]['original_type'];
|
||||
$data[$id][$field]['value'] = _search_api_convert_custom_type($callback, $value, $original_type, $type, $nesting_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->preprocessIndexItems($data);
|
||||
|
||||
return array_merge($ret, $this->server()->indexItems($this, $data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls data alteration hooks for a set of items, according to the index
|
||||
* options.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be altered.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function dataAlter(array &$items) {
|
||||
// First, execute our own search_api_language data alteration.
|
||||
foreach ($items as &$item) {
|
||||
$item->search_api_language = isset($item->language) ? $item->language : LANGUAGE_NONE;
|
||||
}
|
||||
|
||||
foreach ($this->getAlterCallbacks() as $callback) {
|
||||
$callback->alterItems($items);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Property info alter callback that adds the infos of the properties added by
|
||||
* data alter callbacks.
|
||||
*
|
||||
* @param EntityMetadataWrapper $wrapper
|
||||
* The wrapped data.
|
||||
* @param $property_info
|
||||
* The original property info.
|
||||
*
|
||||
* @return array
|
||||
* The altered property info.
|
||||
*/
|
||||
public function propertyInfoAlter(EntityMetadataWrapper $wrapper, array $property_info) {
|
||||
if (entity_get_property_info($wrapper->type())) {
|
||||
// Overwrite the existing properties with the list of properties including
|
||||
// all fields regardless of the used bundle.
|
||||
$property_info['properties'] = entity_get_all_property_info($wrapper->type());
|
||||
}
|
||||
|
||||
if (!isset($this->added_properties)) {
|
||||
$this->added_properties = array(
|
||||
'search_api_language' => array(
|
||||
'label' => t('Item language'),
|
||||
'description' => t("A field added by the search framework to let components determine an item's language. Is always indexed."),
|
||||
'type' => 'token',
|
||||
'options list' => 'entity_metadata_language_list',
|
||||
),
|
||||
);
|
||||
// We use the reverse order here so the hierarchy for overwriting property infos is the same
|
||||
// as for actually overwriting the properties.
|
||||
foreach (array_reverse($this->getAlterCallbacks()) as $callback) {
|
||||
$props = $callback->propertyInfo();
|
||||
if ($props) {
|
||||
$this->added_properties += $props;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Let fields added by data-alter callbacks override default fields.
|
||||
$property_info['properties'] = array_merge($property_info['properties'], $this->added_properties);
|
||||
|
||||
return $property_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the $processors array for use by the pre-/postprocessing functions.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
* @return array
|
||||
* All enabled callbacks for this index, as SearchApiAlterCallbackInterface
|
||||
* objects.
|
||||
*/
|
||||
protected function getAlterCallbacks() {
|
||||
if (isset($this->callbacks)) {
|
||||
return $this->callbacks;
|
||||
}
|
||||
|
||||
$this->callbacks = array();
|
||||
if (empty($this->options['data_alter_callbacks'])) {
|
||||
return $this->callbacks;
|
||||
}
|
||||
$callback_settings = $this->options['data_alter_callbacks'];
|
||||
$infos = search_api_get_alter_callbacks();
|
||||
|
||||
foreach ($callback_settings as $id => $settings) {
|
||||
if (empty($settings['status'])) {
|
||||
continue;
|
||||
}
|
||||
if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
|
||||
watchdog('search_api', t('Undefined data alteration @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
$class = $infos[$id]['class'];
|
||||
$callback = new $class($this, empty($settings['settings']) ? array() : $settings['settings']);
|
||||
if (!($callback instanceof SearchApiAlterCallbackInterface)) {
|
||||
watchdog('search_api', t('Unknown callback class @class specified for data alteration @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->callbacks[$id] = $callback;
|
||||
}
|
||||
return $this->callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* All enabled processors for this index, as SearchApiProcessorInterface
|
||||
* objects.
|
||||
*/
|
||||
protected function getProcessors() {
|
||||
if (isset($this->processors)) {
|
||||
return $this->processors;
|
||||
}
|
||||
|
||||
$this->processors = array();
|
||||
if (empty($this->options['processors'])) {
|
||||
return $this->processors;
|
||||
}
|
||||
$processor_settings = $this->options['processors'];
|
||||
$infos = search_api_get_processors();
|
||||
|
||||
foreach ($processor_settings as $id => $settings) {
|
||||
if (empty($settings['status'])) {
|
||||
continue;
|
||||
}
|
||||
if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
|
||||
watchdog('search_api', t('Undefined processor @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
$class = $infos[$id]['class'];
|
||||
$processor = new $class($this, isset($settings['settings']) ? $settings['settings'] : array());
|
||||
if (!($processor instanceof SearchApiProcessorInterface)) {
|
||||
watchdog('search_api', t('Unknown processor class @class specified for processor @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processors[$id] = $processor;
|
||||
}
|
||||
return $this->processors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess data items for indexing. Data added by data alter callbacks will
|
||||
* be available on the items.
|
||||
*
|
||||
* Typically, a preprocessor will execute its preprocessing (e.g. stemming,
|
||||
* n-grams, word splitting, stripping stop words, etc.) only on the items'
|
||||
* fulltext fields. Other fields should usually be left untouched.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be preprocessed for indexing.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function preprocessIndexItems(array &$items) {
|
||||
foreach ($this->getProcessors() as $processor) {
|
||||
$processor->preprocessIndexItems($items);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Preprocess a search query.
|
||||
*
|
||||
* The same applies as when preprocessing indexed items: typically, only the
|
||||
* fulltext search keys should be processed, queries on specific fields should
|
||||
* usually not be altered.
|
||||
*
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the query to be executed.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function preprocessSearchQuery(SearchApiQuery $query) {
|
||||
foreach ($this->getProcessors() as $processor) {
|
||||
$processor->preprocessSearchQuery($query);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postprocess search results before display.
|
||||
*
|
||||
* If a class is used for both pre- and post-processing a search query, the
|
||||
* same object will be used for both calls (so preserving some data or state
|
||||
* locally is possible).
|
||||
*
|
||||
* @param array $response
|
||||
* An array containing the search results. See
|
||||
* SearchApiServiceInterface->search() for the detailed format.
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the executed query.
|
||||
*
|
||||
* @return SearchApiIndex
|
||||
* The called object.
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
// Postprocessing is done in exactly the opposite direction than preprocessing.
|
||||
foreach (array_reverse($this->getProcessors()) as $processor) {
|
||||
$processor->postprocessSearchResults($response, $query);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all known fields for this index.
|
||||
*
|
||||
* @param $only_indexed (optional)
|
||||
* Return only indexed fields, not all known fields. Defaults to TRUE.
|
||||
* @param $get_additional (optional)
|
||||
* Return not only known/indexed fields, but also related entities whose
|
||||
* fields could additionally be added to the index.
|
||||
*
|
||||
* @return array
|
||||
* An array of all known fields for this index. Keys are the field
|
||||
* identifiers, the values are arrays for specifying the field settings. The
|
||||
* structure of those arrays looks like this:
|
||||
* - name: The human-readable name for the field.
|
||||
* - description: A description of the field, if available.
|
||||
* - indexed: Boolean indicating whether the field is indexed or not.
|
||||
* - type: The type set for this field. One of the types returned by
|
||||
* search_api_default_field_types().
|
||||
* - real_type: (optional) If a custom data type was selected for this
|
||||
* field, this type will be stored here, and "type" contain the fallback
|
||||
* default data type.
|
||||
* - boost: A boost value for terms found in this field during searches.
|
||||
* Usually only relevant for fulltext fields.
|
||||
* - entity_type (optional): If set, the type of this field is really an
|
||||
* entity. The "type" key will then contain "integer", meaning that
|
||||
* servers will ignore this and merely index the entity's ID. Components
|
||||
* displaying this field, though, are advised to use the entity label
|
||||
* instead of the ID.
|
||||
* If $get_additional is TRUE, this array is encapsulated in another
|
||||
* associative array, which contains the above array under the "fields" key,
|
||||
* and a list of related entities (field keys mapped to names) under the
|
||||
* "additional fields" key.
|
||||
*/
|
||||
public function getFields($only_indexed = TRUE, $get_additional = FALSE) {
|
||||
$fields = empty($this->options['fields']) ? array() : $this->options['fields'];
|
||||
$wrapper = $this->entityWrapper();
|
||||
$additional = array();
|
||||
$entity_types = entity_get_info();
|
||||
|
||||
// First we need all already added prefixes.
|
||||
$added = ($only_indexed || empty($this->options['additional fields'])) ? array() : $this->options['additional fields'];
|
||||
foreach (array_keys($fields) as $key) {
|
||||
$len = strlen($key) + 1;
|
||||
$pos = $len;
|
||||
// The third parameter ($offset) to strrpos has rather weird behaviour,
|
||||
// necessitating this rather awkward code. It will iterate over all
|
||||
// prefixes of each field, beginning with the longest, adding all of them
|
||||
// to $added until one is encountered that was already added (which means
|
||||
// all shorter ones will have already been added, too).
|
||||
while ($pos = strrpos($key, ':', $pos - $len)) {
|
||||
$prefix = substr($key, 0, $pos);
|
||||
if (isset($added[$prefix])) {
|
||||
break;
|
||||
}
|
||||
$added[$prefix] = $prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// Then we walk through all properties and look if they are already
|
||||
// contained in one of the arrays.
|
||||
// Since this uses an iterative instead of a recursive approach, it is a bit
|
||||
// complicated, with three arrays tracking the current depth.
|
||||
|
||||
// A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user wrapper
|
||||
$wrappers = array('' => $wrapper);
|
||||
// Display names for the prefixes
|
||||
$prefix_names = array('' => '');
|
||||
// The list nesting level for entities with a certain prefix
|
||||
$nesting_levels = array('' => 0);
|
||||
|
||||
$types = search_api_default_field_types();
|
||||
$flat = array();
|
||||
while ($wrappers) {
|
||||
foreach ($wrappers as $prefix => $wrapper) {
|
||||
$prefix_name = $prefix_names[$prefix];
|
||||
// Deal with lists of entities.
|
||||
$nesting_level = $nesting_levels[$prefix];
|
||||
$type_prefix = str_repeat('list<', $nesting_level);
|
||||
$type_suffix = str_repeat('>', $nesting_level);
|
||||
if ($nesting_level) {
|
||||
$info = $wrapper->info();
|
||||
// The real nesting level of the wrapper, not the accumulated one.
|
||||
$level = search_api_list_nesting_level($info['type']);
|
||||
for ($i = 0; $i < $level; ++$i) {
|
||||
$wrapper = $wrapper[0];
|
||||
}
|
||||
}
|
||||
// Now look at all properties.
|
||||
foreach ($wrapper as $property => $value) {
|
||||
$info = $value->info();
|
||||
// We hide the complexity of multi-valued types from the user here.
|
||||
$type = search_api_extract_inner_type($info['type']);
|
||||
// Treat Entity API type "token" as our "string" type.
|
||||
// Also let text fields with limited options be of type "string" by default.
|
||||
if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) {
|
||||
// Inner type is changed to "string".
|
||||
$type = 'string';
|
||||
// Set the field type accordingly.
|
||||
$info['type'] = search_api_nest_type('string', $info['type']);
|
||||
}
|
||||
$info['type'] = $type_prefix . $info['type'] . $type_suffix;
|
||||
$key = $prefix . $property;
|
||||
if ((isset($types[$type]) || isset($entity_types[$type])) && (!$only_indexed || !empty($fields[$key]))) {
|
||||
if (!empty($fields[$key])) {
|
||||
// This field is already known in the index configuration.
|
||||
$flat[$key] = $fields[$key] + array(
|
||||
'name' => $prefix_name . $info['label'],
|
||||
'description' => empty($info['description']) ? NULL : $info['description'],
|
||||
'boost' => '1.0',
|
||||
'indexed' => TRUE,
|
||||
);
|
||||
// Update the type and its nesting level for non-entity properties.
|
||||
if (!isset($entity_types[$type])) {
|
||||
$flat[$key]['type'] = search_api_nest_type(search_api_extract_inner_type($flat[$key]['type']), $info['type']);
|
||||
if (isset($flat[$key]['real_type'])) {
|
||||
$real_type = search_api_extract_inner_type($flat[$key]['real_type']);
|
||||
$flat[$key]['real_type'] = search_api_nest_type($real_type, $info['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$flat[$key] = array(
|
||||
'name' => $prefix_name . $info['label'],
|
||||
'description' => empty($info['description']) ? NULL : $info['description'],
|
||||
'type' => $info['type'],
|
||||
'boost' => '1.0',
|
||||
'indexed' => FALSE,
|
||||
);
|
||||
}
|
||||
if (isset($entity_types[$type])) {
|
||||
$base_type = isset($entity_types[$type]['entity keys']['name']) ? 'string' : 'integer';
|
||||
$flat[$key]['type'] = search_api_nest_type($base_type, $info['type']);
|
||||
$flat[$key]['entity_type'] = $type;
|
||||
}
|
||||
}
|
||||
if (empty($types[$type])) {
|
||||
if (isset($added[$key])) {
|
||||
// Visit this entity/struct in a later iteration.
|
||||
$wrappers[$key . ':'] = $value;
|
||||
$prefix_names[$key . ':'] = $prefix_name . $info['label'] . ' » ';
|
||||
$nesting_levels[$key . ':'] = search_api_list_nesting_level($info['type']);
|
||||
}
|
||||
else {
|
||||
$name = $prefix_name . $info['label'];
|
||||
// Add machine names to discern fields with identical labels.
|
||||
if (isset($used_names[$name])) {
|
||||
if ($used_names[$name] !== FALSE) {
|
||||
$additional[$used_names[$name]] .= ' [' . $used_names[$name] . ']';
|
||||
$used_names[$name] = FALSE;
|
||||
}
|
||||
$name .= ' [' . $key . ']';
|
||||
}
|
||||
$additional[$key] = $name;
|
||||
$used_names[$name] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($wrappers[$prefix]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$get_additional) {
|
||||
return $flat;
|
||||
}
|
||||
$options = array();
|
||||
$options['fields'] = $flat;
|
||||
$options['additional fields'] = $additional;
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting all of this index's fulltext fields.
|
||||
*
|
||||
* @param boolean $only_indexed
|
||||
* If set to TRUE, only the indexed fulltext fields will be returned.
|
||||
*
|
||||
* @return array
|
||||
* An array containing all (or all indexed) fulltext fields defined for this
|
||||
* index.
|
||||
*/
|
||||
public function getFulltextFields($only_indexed = TRUE) {
|
||||
$i = $only_indexed ? 1 : 0;
|
||||
if (!isset($this->fulltext_fields[$i])) {
|
||||
$this->fulltext_fields[$i] = array();
|
||||
$fields = $only_indexed ? $this->options['fields'] : $this->getFields(FALSE);
|
||||
foreach ($fields as $key => $field) {
|
||||
if (search_api_is_text_type($field['type'])) {
|
||||
$this->fulltext_fields[$i][] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->fulltext_fields[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for creating an entity metadata wrapper appropriate for
|
||||
* this index.
|
||||
*
|
||||
* @param $item
|
||||
* Unless NULL, an item of this index's item type which should be wrapped.
|
||||
* @param $alter
|
||||
* Whether to apply the index's active data alterations on the property
|
||||
* information used. To also apply the data alteration to the wrapped item,
|
||||
* execute SearchApiIndex::dataAlter() on it before calling this method.
|
||||
*
|
||||
* @return EntityMetadataWrapper
|
||||
* A wrapper for the item type of this index, optionally loaded with the
|
||||
* given data and having additional fields according to the data alterations
|
||||
* of this index.
|
||||
*/
|
||||
public function entityWrapper($item = NULL, $alter = TRUE) {
|
||||
$info['property info alter'] = $alter ? array($this, 'propertyInfoAlter') : '_search_api_wrapper_add_all_properties';
|
||||
$info['property defaults']['property info alter'] = '_search_api_wrapper_add_all_properties';
|
||||
return $this->datasource()->getMetadataWrapper($item, $info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to load items from the type lying on this index.
|
||||
*
|
||||
* @param array $ids
|
||||
* The IDs of the items to load.
|
||||
*
|
||||
* @return array
|
||||
* The requested items, as loaded by the data source.
|
||||
*
|
||||
* @see SearchApiDataSourceControllerInterface::loadItems()
|
||||
*/
|
||||
public function loadItems(array $ids) {
|
||||
return $this->datasource()->loadItems($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset internal static caches.
|
||||
*
|
||||
* Should be used when things like fields or data alterations change to avoid
|
||||
* using stale data.
|
||||
*/
|
||||
public function resetCaches() {
|
||||
$this->datasource = NULL;
|
||||
$this->server_object = NULL;
|
||||
$this->callbacks = NULL;
|
||||
$this->processors = NULL;
|
||||
$this->added_properties = NULL;
|
||||
$this->fulltext_fields = array();
|
||||
}
|
||||
|
||||
}
|
418
includes/processor.inc
Normal file
418
includes/processor.inc
Normal file
@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Interface representing a Search API pre- and/or post-processor.
|
||||
*
|
||||
* While processors are enabled or disabled for both pre- and postprocessing at
|
||||
* once, many processors will only need to run in one of those two phases. Then,
|
||||
* the other method(s) should simply be left blank. A processor should make it
|
||||
* clear in its description or documentation when it will run and what effect it
|
||||
* will have.
|
||||
* Usually, processors preprocessing indexed items will likewise preprocess
|
||||
* search queries, so these two methods should mostly be implemented either both
|
||||
* or neither.
|
||||
*/
|
||||
interface SearchApiProcessorInterface {
|
||||
|
||||
/**
|
||||
* Construct a processor.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index for which processing is done.
|
||||
* @param array $options
|
||||
* The processor options set for this index.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array());
|
||||
|
||||
/**
|
||||
* Check whether this processor is applicable for a certain index.
|
||||
*
|
||||
* This can be used for hiding the processor on the index's "Workflow" tab. To
|
||||
* avoid confusion, you should only use criteria that are immutable, such as
|
||||
* the index's item type. Also, since this is only used for UI purposes, you
|
||||
* should not completely rely on this to ensure certain index configurations
|
||||
* and at least throw an exception with a descriptive error message if this is
|
||||
* violated on runtime.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to check for.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if the processor can run on the given index; FALSE otherwise.
|
||||
*/
|
||||
public function supportsIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Display a form for configuring this processor.
|
||||
* Since forcing users to specify options for disabled processors makes no
|
||||
* sense, none of the form elements should have the '#required' attribute set.
|
||||
*
|
||||
* @return array
|
||||
* A form array for configuring this processor, or FALSE if no configuration
|
||||
* is possible.
|
||||
*/
|
||||
public function configurationForm();
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should both return the new options and set them internally.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*
|
||||
* @return array
|
||||
* The new options array for this callback.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Preprocess data items for indexing.
|
||||
*
|
||||
* Typically, a preprocessor will execute its preprocessing (e.g. stemming,
|
||||
* n-grams, word splitting, stripping stop words, etc.) only on the items'
|
||||
* search_api_fulltext fields, if set. Other fields should usually be left
|
||||
* untouched.
|
||||
*
|
||||
* @param array $items
|
||||
* An array of items to be preprocessed for indexing, formatted as specified
|
||||
* by SearchApiServiceInterface::indexItems().
|
||||
*/
|
||||
public function preprocessIndexItems(array &$items);
|
||||
|
||||
/**
|
||||
* Preprocess a search query.
|
||||
*
|
||||
* The same applies as when preprocessing indexed items: typically, only the
|
||||
* fulltext search keys should be processed, queries on specific fields should
|
||||
* usually not be altered.
|
||||
*
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the query to be executed.
|
||||
*/
|
||||
public function preprocessSearchQuery(SearchApiQuery $query);
|
||||
|
||||
/**
|
||||
* Postprocess search results before display.
|
||||
*
|
||||
* If a class is used for both pre- and post-processing a search query, the
|
||||
* same object will be used for both calls (so preserving some data or state
|
||||
* locally is possible).
|
||||
*
|
||||
* @param array $response
|
||||
* An array containing the search results. See the return value of
|
||||
* SearchApiQueryInterface->execute() for the detailed format.
|
||||
* @param SearchApiQuery $query
|
||||
* The object representing the executed query.
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract processor implementation that provides an easy framework for only
|
||||
* processing specific fields.
|
||||
*
|
||||
* Simple processors can just override process(), while others might want to
|
||||
* override the other process*() methods, and test*() (for restricting
|
||||
* processing to something other than all fulltext data).
|
||||
*/
|
||||
abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface {
|
||||
|
||||
/**
|
||||
* @var SearchApiIndex
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Constructor, saving its arguments into properties.
|
||||
*/
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
$this->index = $index;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function supportsIndex(SearchApiIndex $index) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
|
||||
|
||||
$fields = $this->index->getFields();
|
||||
$field_options = array();
|
||||
$default_fields = array();
|
||||
if (isset($this->options['fields'])) {
|
||||
$default_fields = drupal_map_assoc(array_keys($this->options['fields']));
|
||||
}
|
||||
foreach ($fields as $name => $field) {
|
||||
$field_options[$name] = $field['name'];
|
||||
if (!empty($default_fields[$name]) || (!isset($this->options['fields']) && $this->testField($name, $field))) {
|
||||
$default_fields[$name] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$form['fields'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Fields to run on'),
|
||||
'#options' => $field_options,
|
||||
'#default_value' => $default_fields,
|
||||
'#attributes' => array('class' => array('search-api-checkboxes-list')),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
$fields = array_filter($values['fields']);
|
||||
if ($fields) {
|
||||
$fields = array_combine($fields, array_fill(0, count($fields), TRUE));
|
||||
}
|
||||
$values['fields'] = $fields;
|
||||
}
|
||||
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->options = $values;
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls processField() for all appropriate fields.
|
||||
*/
|
||||
public function preprocessIndexItems(array &$items) {
|
||||
foreach ($items as &$item) {
|
||||
foreach ($item as $name => &$field) {
|
||||
if ($this->testField($name, $field)) {
|
||||
$this->processField($field['value'], $field['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls processKeys() for the keys and processFilters() for the filters.
|
||||
*/
|
||||
public function preprocessSearchQuery(SearchApiQuery $query) {
|
||||
$keys = &$query->getKeys();
|
||||
$this->processKeys($keys);
|
||||
$filter = $query->getFilter();
|
||||
$filters = &$filter->getFilters();
|
||||
$this->processFilters($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing.
|
||||
*/
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for preprocessing field data.
|
||||
*
|
||||
* Calls process() either for the whole text, or each token, depending on the
|
||||
* type. Also takes care of extracting list values and of fusing returned
|
||||
* tokens back into a one-dimensional array.
|
||||
*/
|
||||
protected function processField(&$value, &$type) {
|
||||
if (!isset($value) || $value === '') {
|
||||
return;
|
||||
}
|
||||
if (substr($type, 0, 5) == 'list<') {
|
||||
$inner_type = $t = $t1 = substr($type, 5, -1);
|
||||
foreach ($value as &$v) {
|
||||
$t1 = $inner_type;
|
||||
$this->processField($v, $t1);
|
||||
// If one value got tokenized, all others have to follow.
|
||||
if ($t1 != $inner_type) {
|
||||
$t = $t1;
|
||||
}
|
||||
}
|
||||
if ($t == 'tokens') {
|
||||
foreach ($value as $i => &$v) {
|
||||
if (!$v) {
|
||||
unset($value[$i]);
|
||||
continue;
|
||||
}
|
||||
if (!is_array($v)) {
|
||||
$v = array(array('value' => $v, 'score' => 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
$type = "list<$t>";
|
||||
return;
|
||||
}
|
||||
if ($type == 'tokens') {
|
||||
foreach ($value as &$token) {
|
||||
$this->processFieldValue($token['value']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->processFieldValue($value);
|
||||
}
|
||||
if (is_array($value)) {
|
||||
$type = 'tokens';
|
||||
$value = $this->normalizeTokens($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper function for normalizing tokens.
|
||||
*/
|
||||
protected function normalizeTokens($tokens, $score = 1) {
|
||||
$ret = array();
|
||||
foreach ($tokens as $token) {
|
||||
if (empty($token['value']) && !is_numeric($token['value'])) {
|
||||
// Filter out empty tokens.
|
||||
continue;
|
||||
}
|
||||
if (!isset($token['score'])) {
|
||||
$token['score'] = $score;
|
||||
}
|
||||
else {
|
||||
$token['score'] *= $score;
|
||||
}
|
||||
if (is_array($token['value'])) {
|
||||
foreach ($this->normalizeTokens($token['value'], $token['score']) as $t) {
|
||||
$ret[] = $t;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$ret[] = $token;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for preprocessing search keys.
|
||||
*/
|
||||
protected function processKeys(&$keys) {
|
||||
if (is_array($keys)) {
|
||||
foreach ($keys as $key => &$v) {
|
||||
if (element_child($key)) {
|
||||
$this->processKeys($v);
|
||||
if (!$v && !is_numeric($v)) {
|
||||
unset($keys[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->processKey($keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for preprocessing query filters.
|
||||
*/
|
||||
protected function processFilters(array &$filters) {
|
||||
$fields = $this->index->options['fields'];
|
||||
foreach ($filters as &$f) {
|
||||
if (is_array($f)) {
|
||||
if (isset($fields[$f[0]]) && $this->testField($f[0], $fields[$f[0]])) {
|
||||
$this->processFilterValue($f[1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$child_filters = &$f->getFilters();
|
||||
$this->processFilters($child_filters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* The field's machine name.
|
||||
* @param array $field
|
||||
* The field's information.
|
||||
*
|
||||
* @return
|
||||
* TRUE, iff the field should be processed.
|
||||
*/
|
||||
protected function testField($name, array $field) {
|
||||
if (empty($this->options['fields'])) {
|
||||
return $this->testType($field['type']);
|
||||
}
|
||||
return !empty($this->options['fields'][$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
* TRUE, iff the type should be processed.
|
||||
*/
|
||||
protected function testType($type) {
|
||||
return search_api_is_text_type($type, array('text', 'tokens'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for processing a single text element in a field. The default
|
||||
* implementation just calls process().
|
||||
*
|
||||
* $value can either be left a string, or changed into an array of tokens. A
|
||||
* token is an associative array containing:
|
||||
* - value: Either the text inside the token, or a nested array of tokens. The
|
||||
* score of nested tokens will be multiplied by their parent's score.
|
||||
* - score: The relative importance of the token, as a float, with 1 being
|
||||
* the default.
|
||||
*/
|
||||
protected function processFieldValue(&$value) {
|
||||
$this->process($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for processing a single search keyword. The default implementation
|
||||
* just calls process().
|
||||
*
|
||||
* $value can either be left a string, or be changed into a nested keys array,
|
||||
* as defined by SearchApiQueryInterface.
|
||||
*/
|
||||
protected function processKey(&$value) {
|
||||
$this->process($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for processing a single filter value. The default implementation
|
||||
* just calls process().
|
||||
*
|
||||
* $value has to remain a string.
|
||||
*/
|
||||
protected function processFilterValue(&$value) {
|
||||
$this->process($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that is ultimately called for all text by the standard
|
||||
* implementation, and does nothing by default.
|
||||
*
|
||||
* @param $value
|
||||
* The value to preprocess as a string. Can be manipulated directly, nothing
|
||||
* has to be returned. Since this can be called for all value types, $value
|
||||
* has to remain a string.
|
||||
*/
|
||||
protected function process(&$value) {
|
||||
|
||||
}
|
||||
|
||||
}
|
137
includes/processor_html_filter.inc
Normal file
137
includes/processor_html_filter.inc
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Processor for stripping HTML from indexed fulltext data. Supports assigning
|
||||
* custom boosts for any HTML element.
|
||||
*/
|
||||
// @todo Process query?
|
||||
class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
public function __construct(SearchApiIndex $index, array $options = array()) {
|
||||
parent::__construct($index, $options);
|
||||
$this->options += array(
|
||||
'title' => FALSE,
|
||||
'alt' => TRUE,
|
||||
'tags' => "h1 = 5\n" .
|
||||
"h2 = 3\n" .
|
||||
"h3 = 2\n" .
|
||||
"strong = 2\n" .
|
||||
"b = 2\n" .
|
||||
"em = 1.5\n" .
|
||||
'u = 1.5',
|
||||
);
|
||||
$this->tags = drupal_parse_info_format($this->options['tags']);
|
||||
// Specifying empty tags doesn't make sense.
|
||||
unset($this->tags['br'], $this->tags['hr']);
|
||||
}
|
||||
|
||||
public function configurationForm() {
|
||||
$form = parent::configurationForm();
|
||||
$form += array(
|
||||
'title' => array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Index title attribute'),
|
||||
'#description' => t('If set, the contents of title attributes will be indexed.'),
|
||||
'#default_value' => $this->options['title'],
|
||||
),
|
||||
'alt' => array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Index alt attribute'),
|
||||
'#description' => t('If set, the alternative text of images will be indexed.'),
|
||||
'#default_value' => $this->options['alt'],
|
||||
),
|
||||
'tags' => array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Tag boosts'),
|
||||
'#description' => t('Specify special boost values for certain HTML elements, in <a href="@link">INI file format</a>. ' .
|
||||
'The boost values of nested elements are multiplied, elements not mentioned will have the default boost value of 1. ' .
|
||||
'Assign a boost of 0 to ignore the text content of that HTML element.',
|
||||
array('@link' => url('http://api.drupal.org/api/function/drupal_parse_info_format/7'))),
|
||||
'#default_value' => $this->options['tags'],
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
parent::configurationFormValidate($form, $values, $form_state);
|
||||
|
||||
if (empty($values['tags'])) {
|
||||
return;
|
||||
}
|
||||
$tags = drupal_parse_info_format($values['tags']);
|
||||
$errors = array();
|
||||
foreach ($tags as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$errors[] = t("Boost value for tag <@tag> can't be an array.", array('@tag' => $key));
|
||||
}
|
||||
elseif (!is_numeric($value)) {
|
||||
$errors[] = t("Boost value for tag <@tag> must be numeric.", array('@tag' => $key));
|
||||
}
|
||||
elseif ($value < 0) {
|
||||
$errors[] = t('Boost value for tag <@tag> must be non-negative.', array('@tag' => $key));
|
||||
}
|
||||
}
|
||||
if ($errors) {
|
||||
form_error($form['tags'], implode("<br />\n", $errors));
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFieldValue(&$value) {
|
||||
$text = str_replace(array('<', '>'), array(' <', '> '), $value); // Let removed tags still delimit words.
|
||||
if ($this->options['title']) {
|
||||
$text = preg_replace('/(<[-a-z_]+[^>]+)\btitle\s*=\s*("([^"]+)"|\'([^\']+)\')([^>]*>)/i', '$1 $5 $3$4 ', $text);
|
||||
}
|
||||
if ($this->options['alt']) {
|
||||
$text = preg_replace('/<img\b[^>]+\balt\s*=\s*("([^"]+)"|\'([^\']+)\')[^>]*>/i', ' <img>$2$3</img> ', $text);
|
||||
}
|
||||
if ($this->tags) {
|
||||
$text = strip_tags($text, '<' . implode('><', array_keys($this->tags)) . '>');
|
||||
$value = $this->parseText($text);
|
||||
}
|
||||
else {
|
||||
$value = strip_tags($text);
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseText(&$text, $active_tag = NULL, $boost = 1) {
|
||||
$ret = array();
|
||||
while (($pos = strpos($text, '<')) !== FALSE) {
|
||||
if ($boost && $pos > 0) {
|
||||
$ret[] = array(
|
||||
'value' => html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'),
|
||||
'score' => $boost,
|
||||
);
|
||||
}
|
||||
$text = substr($text, $pos + 1);
|
||||
preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m);
|
||||
$text = substr($text, strpos($text, '>') + 1);
|
||||
if ($m[1]) {
|
||||
// Closing tag.
|
||||
if ($active_tag && $m[2] == $active_tag) {
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Opening tag => recursive call.
|
||||
$inner_boost = $boost * (isset($this->tags[$m[2]]) ? $this->tags[$m[2]] : 1);
|
||||
$ret = array_merge($ret, $this->parseText($text, $m[2], $inner_boost));
|
||||
}
|
||||
}
|
||||
if ($text) {
|
||||
$ret[] = array(
|
||||
'value' => html_entity_decode($text, ENT_QUOTES, 'UTF-8'),
|
||||
'score' => $boost,
|
||||
);
|
||||
$text = '';
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
12
includes/processor_ignore_case.inc
Normal file
12
includes/processor_ignore_case.inc
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Processor for making searches case-insensitive.
|
||||
*/
|
||||
class SearchApiIgnoreCase extends SearchApiAbstractProcessor {
|
||||
|
||||
protected function process(&$value) {
|
||||
$value = drupal_strtolower($value);
|
||||
}
|
||||
|
||||
}
|
94
includes/processor_stopwords.inc
Normal file
94
includes/processor_stopwords.inc
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Processor for removing stopwords from index and search terms.
|
||||
*/
|
||||
class SearchApiStopWords extends SearchApiAbstractProcessor {
|
||||
|
||||
public function configurationForm() {
|
||||
$form = parent::configurationForm();
|
||||
|
||||
$form += array(
|
||||
'help' => array(
|
||||
'#markup' => '<p>' . t('Provide a stopwords file or enter the words in this form. If you do both, both will be used. Read about !stopwords.', array('!stopwords' => l(t('stop words'), "http://en.wikipedia.org/wiki/Stop_words"))) . '</p>',
|
||||
),
|
||||
'file' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Stopwords file URI'),
|
||||
'#title' => t('Enter the URI of your stopwords.txt file'),
|
||||
'#description' => t('This must be a stream-type description like <code>public://stopwords/stopwords.txt</code> or <code>http://example.com/stopwords.txt</code> or <code>private://stopwords.txt</code>.'),
|
||||
),
|
||||
'stopwords' => array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Stopwords'),
|
||||
'#description' => t('Enter a space and/or linebreak separated list of stopwords that will be removed from content before it is indexed and from search terms before searching.'),
|
||||
'#default_value' => t("but\ndid\nthe this that those\netc"),
|
||||
),
|
||||
);
|
||||
|
||||
if (!empty($this->options)) {
|
||||
$form['file']['#default_value'] = $this->options['file'];
|
||||
$form['stopwords']['#default_value'] = $this->options['stopwords'];
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
parent::configurationFormValidate($form, $values, $form_state);
|
||||
|
||||
$stopwords = trim($values['stopwords']);
|
||||
$uri = $values['file'];
|
||||
if (empty($stopwords) && empty($uri)) {
|
||||
$el = $form['file'];
|
||||
form_error($el, $el['#title'] . ': ' . t('At stopwords file or words are required.'));
|
||||
}
|
||||
if (!empty($uri) && !file_get_contents($uri)) {
|
||||
$el = $form['file'];
|
||||
form_error($el, t('Stopwords file') . ': ' . t('The file %uri is not readable or does not exist.', array('%uri' => $uri)));
|
||||
}
|
||||
}
|
||||
|
||||
public function process(&$value) {
|
||||
$stopwords = $this->getStopWords();
|
||||
if (empty($stopwords)) {
|
||||
return;
|
||||
}
|
||||
$words = preg_split('/\s+/', $value);
|
||||
foreach ($words as $sub_key => $sub_value) {
|
||||
if (isset($stopwords[$sub_value])) {
|
||||
unset($words[$sub_key]);
|
||||
$this->ignored[] = $sub_value;
|
||||
}
|
||||
}
|
||||
$value = implode(' ', $words);
|
||||
}
|
||||
|
||||
public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
|
||||
if (isset($this->ignored)) {
|
||||
if (isset($response['ignored'])) {
|
||||
$response['ignored'] = array_merge($response['ignored'], $this->ignored);
|
||||
}
|
||||
else $response['ignored'] = $this->ignored;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
* An array whose keys are the stopwords set in either the file or the text
|
||||
* field.
|
||||
*/
|
||||
protected function getStopWords() {
|
||||
if (isset($this->stopwords)) {
|
||||
return $this->stopwords;
|
||||
}
|
||||
$file_words = $form_words = array();
|
||||
if (!empty($this->options['file']) && $stopwords_file = file_get_contents($this->options['file'])) {
|
||||
$file_words = preg_split('/\s+/', $stopwords_file);
|
||||
}
|
||||
if (!empty($this->options['stopwords'])) {
|
||||
$form_words = preg_split('/\s+/', $this->options['stopwords']);
|
||||
}
|
||||
$this->stopwords = array_flip(array_merge($file_words, $form_words));
|
||||
return $this->stopwords;
|
||||
}
|
||||
}
|
95
includes/processor_tokenizer.inc
Normal file
95
includes/processor_tokenizer.inc
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Processor for tokenizing fulltext data by replacing (configurable)
|
||||
* non-letters with spaces.
|
||||
*/
|
||||
class SearchApiTokenizer extends SearchApiAbstractProcessor {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $spaces;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $ignorable;
|
||||
|
||||
public function configurationForm() {
|
||||
$form = parent::configurationForm();
|
||||
$form += array(
|
||||
'spaces' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Whitespace characters'),
|
||||
'#description' => t('Specify the characters that should be regarded as whitespace and therefore used as word-delimiters. ' .
|
||||
'Specify the characters as a <a href="@link">PCRE character class</a>. ' .
|
||||
'Note: For non-English content, the default setting might not be suitable.',
|
||||
array('@link' => url('http://www.php.net/manual/en/regexp.reference.character-classes.php'))),
|
||||
'#default_value' => "[^[:alnum:]]",
|
||||
),
|
||||
'ignorable' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Ignorable characters'),
|
||||
'#description' => t('Specify characters which should be removed from fulltext fields and search strings (e.g., "-"). The same format as above is used.'),
|
||||
'#default_value' => "[']",
|
||||
),
|
||||
);
|
||||
|
||||
if (!empty($this->options)) {
|
||||
$form['spaces']['#default_value'] = $this->options['spaces'];
|
||||
$form['ignorable']['#default_value'] = $this->options['ignorable'];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
parent::configurationFormValidate($form, $values, $form_state);
|
||||
|
||||
$spaces = str_replace('/', '\/', $values['spaces']);
|
||||
$ignorable = str_replace('/', '\/', $values['ignorable']);
|
||||
if (@preg_match('/(' . $spaces . ')+/u', '') === FALSE) {
|
||||
$el = $form['spaces'];
|
||||
form_error($el, $el['#title'] . ': ' . t('The entered text is no valid regular expression.'));
|
||||
}
|
||||
if (@preg_match('/(' . $ignorable . ')+/u', '') === FALSE) {
|
||||
$el = $form['ignorable'];
|
||||
form_error($el, $el['#title'] . ': ' . t('The entered text is no valid regular expression.'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function processFieldValue(&$value) {
|
||||
$this->prepare();
|
||||
if ($this->ignorable) {
|
||||
$value = preg_replace('/(' . $this->ignorable . ')+/u', '', $value);
|
||||
}
|
||||
if ($this->spaces) {
|
||||
$arr = preg_split('/(' . $this->spaces . ')+/u', $value);
|
||||
if (count($arr) > 1) {
|
||||
$value = array();
|
||||
foreach ($arr as $token) {
|
||||
$value[] = array('value' => $token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function process(&$value) {
|
||||
$this->prepare();
|
||||
if ($this->ignorable) {
|
||||
$value = preg_replace('/' . $this->ignorable . '+/u', '', $value);
|
||||
}
|
||||
if ($this->spaces) {
|
||||
$value = preg_replace('/' . $this->spaces . '+/u', ' ', $value);
|
||||
}
|
||||
}
|
||||
|
||||
protected function prepare() {
|
||||
if (!isset($this->spaces)) {
|
||||
$this->spaces = str_replace('/', '\/', $this->options['spaces']);
|
||||
$this->ignorable = str_replace('/', '\/', $this->options['ignorable']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1066
includes/query.inc
Normal file
1066
includes/query.inc
Normal file
File diff suppressed because it is too large
Load Diff
228
includes/server_entity.inc
Normal file
228
includes/server_entity.inc
Normal file
@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class representing a search server.
|
||||
*
|
||||
* This can handle the same calls as defined in the SearchApiServiceInterface
|
||||
* and pass it on to the service implementation appropriate for this server.
|
||||
*/
|
||||
class SearchApiServer extends Entity {
|
||||
|
||||
/* Database values that will be set when object is loaded: */
|
||||
|
||||
/**
|
||||
* The primary identifier for a server.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $id = 0;
|
||||
|
||||
/**
|
||||
* The displayed name for a server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = '';
|
||||
|
||||
/**
|
||||
* The machine name for a server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $machine_name = '';
|
||||
|
||||
/**
|
||||
* The displayed description for a server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $description = '';
|
||||
|
||||
/**
|
||||
* The id of the service class to use for this server.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $class = '';
|
||||
|
||||
/**
|
||||
* The options used to configure the service object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $options = array();
|
||||
|
||||
/**
|
||||
* A flag indicating whether the server is enabled.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $enabled = 1;
|
||||
|
||||
/**
|
||||
* Proxy object for invoking service methods.
|
||||
*
|
||||
* @var SearchApiServiceInterface
|
||||
*/
|
||||
protected $proxy;
|
||||
|
||||
/**
|
||||
* Constructor as a helper to the parent constructor.
|
||||
*/
|
||||
public function __construct(array $values = array()) {
|
||||
parent::__construct($values, 'search_api_server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for updating entity properties.
|
||||
*
|
||||
* NOTE: You shouldn't change any properties of this object before calling
|
||||
* this method, as this might lead to the fields not being saved correctly.
|
||||
*
|
||||
* @param array $fields
|
||||
* The new field values.
|
||||
*
|
||||
* @return
|
||||
* SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
|
||||
* the specified values.
|
||||
*/
|
||||
public function update(array $fields) {
|
||||
$changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'options' => 1);
|
||||
$changed = FALSE;
|
||||
foreach ($fields as $field => $value) {
|
||||
if (isset($changeable[$field]) && $value !== $this->$field) {
|
||||
$this->$field = $value;
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
// If there are no new values, just return 0.
|
||||
if (!$changed) {
|
||||
return 0;
|
||||
}
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic method for determining which fields should be serialized.
|
||||
*
|
||||
* Serialize all properties except the proxy object.
|
||||
*
|
||||
* @return array
|
||||
* An array of properties to be serialized.
|
||||
*/
|
||||
public function __sleep() {
|
||||
$ret = get_object_vars($this);
|
||||
unset($ret['proxy'], $ret['status'], $ret['module'], $ret['is_new']);
|
||||
return array_keys($ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for ensuring the proxy object is set up.
|
||||
*/
|
||||
protected function ensureProxy() {
|
||||
if (!isset($this->proxy)) {
|
||||
$class = search_api_get_service_info($this->class);
|
||||
if ($class && class_exists($class['class'])) {
|
||||
if (empty($this->options)) {
|
||||
// We always have to provide the options.
|
||||
$this->options = array();
|
||||
}
|
||||
$this->proxy = new $class['class']($this);
|
||||
}
|
||||
if (!($this->proxy instanceof SearchApiServiceInterface)) {
|
||||
throw new SearchApiException(t('Search server with machine name @name specifies illegal service class @class.', array('@name' => $this->machine_name, '@class' => $this->class)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the service class defines additional methods, not specified in the
|
||||
* SearchApiServiceInterface interface, then they are called via this magic
|
||||
* method.
|
||||
*/
|
||||
public function __call($name, $arguments = array()) {
|
||||
$this->ensureProxy();
|
||||
return call_user_func_array(array($this->proxy, $name), $arguments);
|
||||
}
|
||||
|
||||
// Proxy methods
|
||||
|
||||
// For increased clarity, and since some parameters are passed by reference,
|
||||
// we don't use the __call() magic method for those.
|
||||
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationForm($form, $form_state);
|
||||
}
|
||||
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationFormValidate($form, $values, $form_state);
|
||||
}
|
||||
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->configurationFormSubmit($form, $values, $form_state);
|
||||
}
|
||||
|
||||
public function supportsFeature($feature) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->supportsFeature($feature);
|
||||
}
|
||||
|
||||
public function viewSettings() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->viewSettings();
|
||||
}
|
||||
|
||||
public function postCreate() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->postCreate();
|
||||
}
|
||||
|
||||
public function postUpdate() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->postUpdate();
|
||||
}
|
||||
|
||||
public function preDelete() {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->preDelete();
|
||||
}
|
||||
|
||||
public function addIndex(SearchApiIndex $index) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->addIndex($index);
|
||||
}
|
||||
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->fieldsUpdated($index);
|
||||
}
|
||||
|
||||
public function removeIndex($index) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->removeIndex($index);
|
||||
}
|
||||
|
||||
public function indexItems(SearchApiIndex $index, array $items) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->indexItems($index, $items);
|
||||
}
|
||||
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->deleteItems($ids, $index);
|
||||
}
|
||||
|
||||
public function query(SearchApiIndex $index, $options = array()) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->query($index, $options);
|
||||
}
|
||||
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
$this->ensureProxy();
|
||||
return $this->proxy->search($query);
|
||||
}
|
||||
|
||||
}
|
469
includes/service.inc
Normal file
469
includes/service.inc
Normal file
@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Interface defining the methods search services have to implement.
|
||||
*
|
||||
* Before a service object is used, the corresponding server's data will be read
|
||||
* from the database (see SearchApiAbstractService for a list of fields).
|
||||
*/
|
||||
interface SearchApiServiceInterface {
|
||||
|
||||
/**
|
||||
* Constructor for a service class, setting the server configuration used with
|
||||
* this service.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The server object for this service.
|
||||
*/
|
||||
public function __construct(SearchApiServer $server);
|
||||
|
||||
/**
|
||||
* Form callback. Might be called on an uninitialized object - in this case,
|
||||
* the form is for configuring a newly created server.
|
||||
*
|
||||
* @return array
|
||||
* A form array for setting service-specific options.
|
||||
*/
|
||||
public function configurationForm(array $form, array &$form_state);
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* $form_state['server'] will contain the server that is created or edited.
|
||||
* Use form_error() to flag errors on form elements.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* This method should set the options of this service' server according to
|
||||
* $values.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state);
|
||||
|
||||
/**
|
||||
* Determines whether this service class implementation supports a given
|
||||
* feature. Features are optional extensions to Search API functionality and
|
||||
* usually defined and used by third-party modules.
|
||||
*
|
||||
* There are currently three features defined directly in the Search API
|
||||
* project:
|
||||
* - "search_api_facets", by the search_api_facetapi module.
|
||||
* - "search_api_facets_operator_or", also by the search_api_facetapi module.
|
||||
* - "search_api_mlt", by the search_api_views module.
|
||||
* Other contrib modules might define additional features. These should always
|
||||
* be properly documented in the module by which they are defined.
|
||||
*
|
||||
* @param string $feature
|
||||
* The name of the optional feature.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature);
|
||||
|
||||
/**
|
||||
* View this server's settings. Output can be HTML or a render array, a <dl>
|
||||
* listing all relevant settings is preferred.
|
||||
*/
|
||||
public function viewSettings();
|
||||
|
||||
/**
|
||||
* Called once, when the server is first created. Allows it to set up its
|
||||
* necessary infrastructure.
|
||||
*/
|
||||
public function postCreate();
|
||||
|
||||
/**
|
||||
* Notifies this server that its fields are about to be updated. The server's
|
||||
* $original property can be used to inspect the old property values.
|
||||
*
|
||||
* @return
|
||||
* TRUE, if the update requires reindexing of all content on the server.
|
||||
*/
|
||||
public function postUpdate();
|
||||
|
||||
/**
|
||||
* Notifies this server that it is about to be deleted from the database and
|
||||
* should therefore clean up, if appropriate.
|
||||
*
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Add 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 SearchApiIndex $index
|
||||
* The index to add.
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Notify the server that the indexed field settings for the index have
|
||||
* changed.
|
||||
* If any user action is necessary as a result of this, the method should
|
||||
* use drupal_set_message() to notify the user.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The updated index.
|
||||
*
|
||||
* @return
|
||||
* TRUE, if this change affected the server in any way that forces it to
|
||||
* re-index the content. FALSE otherwise.
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index);
|
||||
|
||||
/**
|
||||
* Remove 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->server.
|
||||
*
|
||||
* If the index wasn't added to the server, the method call should be ignored.
|
||||
*
|
||||
* @param $index
|
||||
* Either an object representing the index to remove, or its machine name
|
||||
* (if the index was completely deleted).
|
||||
*/
|
||||
public function removeIndex($index);
|
||||
|
||||
/**
|
||||
* Index the specified items.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The search index for which items should be indexed.
|
||||
* @param array $items
|
||||
* An array of items to be indexed, keyed by their id. The values are
|
||||
* associative arrays of the fields to be stored, where each field is an
|
||||
* array with the following keys:
|
||||
* - type: One of the data types recognized by the Search API, or the
|
||||
* special type "tokens" for fulltext fields.
|
||||
* - original_type: The original type of the property, as defined by the
|
||||
* datasource controller for the index's item type.
|
||||
* - value: The value to index.
|
||||
*
|
||||
* The special field "search_api_language" contains the item's language and
|
||||
* should always be indexed.
|
||||
*
|
||||
* The value of fields with the "tokens" type is an array of tokens. Each
|
||||
* token is an array containing the following keys:
|
||||
* - value: The word that the token represents.
|
||||
* - score: A score for the importance of that word.
|
||||
*
|
||||
* @return array
|
||||
* An array of the ids of all items that were successfully indexed.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If indexing was prevented by a fundamental configuration error.
|
||||
*/
|
||||
public function indexItems(SearchApiIndex $index, array $items);
|
||||
|
||||
/**
|
||||
* Delete items from an index on this server.
|
||||
*
|
||||
* Might be either used to delete some items (given by their ids) from a
|
||||
* specified index, or all items from that index, or all items from all
|
||||
* indexes on this server.
|
||||
*
|
||||
* @param $ids
|
||||
* Either an array containing the ids of the items that should be deleted,
|
||||
* or 'all' if all items should be deleted. Other formats might be
|
||||
* recognized by implementing classes, but these are not standardized.
|
||||
* @param SearchApiIndex $index
|
||||
* The index from which items should be deleted, or NULL if all indexes on
|
||||
* this server should be cleared (then, $ids has to be 'all').
|
||||
*/
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL);
|
||||
|
||||
/**
|
||||
* Create a query object for searching on an index lying on this server.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to search on.
|
||||
* @param $options
|
||||
* Associative array of options configuring this query. See
|
||||
* SearchApiQueryInterface::__construct().
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* An object for searching the given index.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the server is currently disabled.
|
||||
*/
|
||||
public function query(SearchApiIndex $index, $options = array());
|
||||
|
||||
/**
|
||||
* Executes a search on the server represented by this object.
|
||||
*
|
||||
* @param $query
|
||||
* The SearchApiQueryInterface object to execute.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing the search results, as required by
|
||||
* SearchApiQueryInterface::execute().
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If an error prevented the search from completing.
|
||||
*/
|
||||
public function search(SearchApiQueryInterface $query);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class with generic implementation of most service methods.
|
||||
*/
|
||||
abstract class SearchApiAbstractService implements SearchApiServiceInterface {
|
||||
|
||||
/**
|
||||
* @var SearchApiServer
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* Direct reference to the server's $options property.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = array();
|
||||
|
||||
/**
|
||||
* Constructor for a service class, setting the server configuration used with
|
||||
* this service.
|
||||
*
|
||||
* The default implementation sets $this->server and $this->options.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The server object for this service.
|
||||
*/
|
||||
public function __construct(SearchApiServer $server) {
|
||||
$this->server = $server;
|
||||
$this->options = &$server->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form callback. Might be called on an uninitialized object - in this case,
|
||||
* the form is for configuring a newly created server.
|
||||
*
|
||||
* Returns an empty form by default.
|
||||
*
|
||||
* @return array
|
||||
* A form array for setting service-specific options.
|
||||
*/
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation callback for the form returned by configurationForm().
|
||||
*
|
||||
* Does nothing by default.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for the form returned by configurationForm().
|
||||
*
|
||||
* The default implementation just ensures that additional elements in
|
||||
* $options, not present in the form, don't get lost at the update.
|
||||
*
|
||||
* @param array $form
|
||||
* The form returned by configurationForm().
|
||||
* @param array $values
|
||||
* The part of the $form_state['values'] array corresponding to this form.
|
||||
* @param array $form_state
|
||||
* The complete form state.
|
||||
*/
|
||||
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
|
||||
if (!empty($this->options)) {
|
||||
$values += $this->options;
|
||||
}
|
||||
$this->options = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this service class implementation supports a given
|
||||
* feature. Features are optional extensions to Search API functionality and
|
||||
* usually defined and used by third-party modules.
|
||||
*
|
||||
* There are currently three features defined directly in the Search API
|
||||
* project:
|
||||
* - "search_api_facets", by the search_api_facetapi module.
|
||||
* - "search_api_facets_operator_or", also by the search_api_facetapi module.
|
||||
* - "search_api_mlt", by the search_api_views module.
|
||||
* Other contrib modules might define additional features. These should always
|
||||
* be properly documented in the module by which they are defined.
|
||||
*
|
||||
* @param string $feature
|
||||
* The name of the optional feature.
|
||||
*
|
||||
* @return boolean
|
||||
* TRUE if this service knows and supports the specified feature. FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function supportsFeature($feature) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* View this server's settings. Output can be HTML or a render array, a <dl>
|
||||
* listing all relevant settings is preferred.
|
||||
*
|
||||
* The default implementation does a crude output as a definition list, with
|
||||
* option names taken from the configuration form.
|
||||
*/
|
||||
public function viewSettings() {
|
||||
$output = '';
|
||||
$form = $form_state = array();
|
||||
$option_form = $this->configurationForm($form, $form_state);
|
||||
$option_names = array();
|
||||
foreach ($option_form as $key => $element) {
|
||||
if (isset($element['#title']) && isset($this->options[$key])) {
|
||||
$option_names[$key] = $element['#title'];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($option_names as $key => $name) {
|
||||
$value = $this->options[$key];
|
||||
$output .= '<dt>' . check_plain($name) . '</dt>' . "\n";
|
||||
$output .= '<dd>' . nl2br(check_plain(print_r($value, TRUE))) . '</dd>' . "\n";
|
||||
}
|
||||
|
||||
return $output ? "<dl>\n$output</dl>" : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once, when the server is first created. Allows it to set up its
|
||||
* necessary infrastructure.
|
||||
*
|
||||
* Does nothing, by default.
|
||||
*/
|
||||
public function postCreate() {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this server that its fields are about to be updated. The server's
|
||||
* $original property can be used to inspect the old property values.
|
||||
*
|
||||
* @return
|
||||
* TRUE, if the update requires reindexing of all content on the server.
|
||||
*/
|
||||
public function postUpdate() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this server that it is about to be deleted from the database and
|
||||
* should therefore clean up, if appropriate.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* By default, deletes all indexes from this server.
|
||||
*/
|
||||
public function preDelete() {
|
||||
$indexes = search_api_index_load_multiple(FALSE, array('server' => $this->server->machine_name));
|
||||
foreach ($indexes as $index) {
|
||||
$this->removeIndex($index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new index to this server.
|
||||
*
|
||||
* Does nothing, by default.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to add.
|
||||
*/
|
||||
public function addIndex(SearchApiIndex $index) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the server that the indexed field settings for the index have
|
||||
* changed.
|
||||
* If any user action is necessary as a result of this, the method should
|
||||
* use drupal_set_message() to notify the user.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The updated index.
|
||||
*
|
||||
* @return
|
||||
* TRUE, if this change affected the server in any way that forces it to
|
||||
* re-index the content. FALSE otherwise.
|
||||
*/
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove 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->server.
|
||||
*
|
||||
* By default, removes all items from that index.
|
||||
*
|
||||
* @param $index
|
||||
* Either an object representing the index to remove, or its machine name
|
||||
* (if the index was completely deleted).
|
||||
*/
|
||||
public function removeIndex($index) {
|
||||
$this->deleteItems('all', $index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query object for searching on an index lying on this server.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The index to search on.
|
||||
* @param $options
|
||||
* Associative array of options configuring this query. See
|
||||
* SearchApiQueryInterface::__construct().
|
||||
*
|
||||
* @return SearchApiQueryInterface
|
||||
* An object for searching the given index.
|
||||
*
|
||||
* @throws SearchApiException
|
||||
* If the server is currently disabled.
|
||||
*/
|
||||
public function query(SearchApiIndex $index, $options = array()) {
|
||||
return new SearchApiQuery($index, $options);
|
||||
}
|
||||
|
||||
}
|
44
search_api.admin.css
Normal file
44
search_api.admin.css
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
td.search-api-status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.search-api-edit-menu {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
color: black;
|
||||
z-index: 999;
|
||||
border: 1px solid black;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
-khtml-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
div.search-api-edit-menu ul {
|
||||
margin: 0 0.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.search-api-edit-menu ul li {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.search-api-edit-menu.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-api-alter-add-aggregation-fields,
|
||||
.search-api-checkboxes-list {
|
||||
max-height: 12em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Workaround for http://drupal.org/node/1015798 */
|
||||
.vertical-tabs fieldset div.fieldset-wrapper fieldset legend {
|
||||
display: block;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
1970
search_api.admin.inc
Normal file
1970
search_api.admin.inc
Normal file
File diff suppressed because it is too large
Load Diff
61
search_api.admin.js
Normal file
61
search_api.admin.js
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
// Copied from filter.admin.js
|
||||
(function ($) {
|
||||
|
||||
Drupal.behaviors.searchApiStatus = {
|
||||
attach: function (context, settings) {
|
||||
$('.search-api-status-wrapper input.form-checkbox', context).once('search-api-status', function () {
|
||||
var $checkbox = $(this);
|
||||
// Retrieve the tabledrag row belonging to this processor.
|
||||
var $row = $('#' + $checkbox.attr('id').replace(/-status$/, '-weight'), context).closest('tr');
|
||||
// Retrieve the vertical tab belonging to this processor.
|
||||
var $tab = $('#' + $checkbox.attr('id').replace(/-status$/, '-settings'), context).data('verticalTab');
|
||||
|
||||
// Bind click handler to this checkbox to conditionally show and hide the
|
||||
// filter's tableDrag row and vertical tab pane.
|
||||
$checkbox.bind('click.searchApiUpdate', function () {
|
||||
if ($checkbox.is(':checked')) {
|
||||
$row.show();
|
||||
if ($tab) {
|
||||
$tab.tabShow().updateSummary();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$row.hide();
|
||||
if ($tab) {
|
||||
$tab.tabHide().updateSummary();
|
||||
}
|
||||
}
|
||||
// Restripe table after toggling visibility of table row.
|
||||
Drupal.tableDrag['search-api-' + $checkbox.attr('id').replace(/^edit-([^-]+)-.*$/, '$1') + '-order-table'].restripeTable();
|
||||
});
|
||||
|
||||
// Attach summary for configurable items (only for screen-readers).
|
||||
if ($tab) {
|
||||
$tab.fieldset.drupalSetSummary(function (tabContext) {
|
||||
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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Drupal.behaviors.searchApiEditMenu = {
|
||||
attach: function (context, settings) {
|
||||
$('.search-api-edit-menu-toggle', context).click(function (e) {
|
||||
$menu = $(this).parent().find('.search-api-edit-menu');
|
||||
if ($menu.is('.collapsed')) {
|
||||
$menu.removeClass('collapsed');
|
||||
}
|
||||
else {
|
||||
$menu.addClass('collapsed');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
535
search_api.api.php
Normal file
535
search_api.api.php
Normal file
@ -0,0 +1,535 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Search API module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines one or more search service classes a module offers.
|
||||
*
|
||||
* Note: The ids should be valid PHP identifiers.
|
||||
*
|
||||
* @see hook_search_api_service_info_alter()
|
||||
*
|
||||
* @return array
|
||||
* An associative array of search service classes, keyed by a unique
|
||||
* identifier and containing associative arrays with the following keys:
|
||||
* - name: The service class' translated name.
|
||||
* - description: A translated string to be shown to administrators when
|
||||
* selecting a service class. Should contain all peculiarities of the
|
||||
* service class, like field type support, supported features (like facets),
|
||||
* the "direct" parse mode and other specific things to keep in mind.
|
||||
* - class: The service class, which has to implement the
|
||||
* SearchApiServiceInterface interface.
|
||||
*/
|
||||
function hook_search_api_service_info() {
|
||||
$services['example_some'] = array(
|
||||
'name' => t('Some Service'),
|
||||
'description' => t('Service for some search engine.'),
|
||||
'class' => 'SomeServiceClass',
|
||||
// Unknown keys can later be read by the object for additional information.
|
||||
'init args' => array('foo' => 'Foo', 'bar' => 42),
|
||||
);
|
||||
$services['example_other'] = array(
|
||||
'name' => t('Other Service'),
|
||||
'description' => t('Service for another search engine.'),
|
||||
'class' => 'OtherServiceClass',
|
||||
);
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the Search API service info.
|
||||
*
|
||||
* Modules may implement this hook to alter the information that defines Search
|
||||
* API service. All properties that are available in
|
||||
* hook_search_api_service_info() can be altered here.
|
||||
*
|
||||
* @see hook_search_api_service_info()
|
||||
*
|
||||
* @param array $service_info
|
||||
* The Search API service info array, keyed by service id.
|
||||
*/
|
||||
function hook_search_api_service_info_alter(array &$service_info) {
|
||||
foreach ($service_info as $id => $info) {
|
||||
$service_info[$id]['class'] = 'MyProxyServiceClass';
|
||||
$service_info[$id]['example_original_class'] = $info['class'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define new types of items that can be searched.
|
||||
*
|
||||
* This hook allows modules to define their own item types, for which indexes
|
||||
* can then be created. (Note that the Search API natively provides support for
|
||||
* all entity types that specify property information, so they should not be
|
||||
* added here. You should therefore also not use an existing entity type as the
|
||||
* identifier of a new item type.)
|
||||
*
|
||||
* The main part of defining a new item type is implementing its data source
|
||||
* controller class, which is responsible for loading items, providing metadata
|
||||
* and tracking existing items. The module defining a certain item type is also
|
||||
* responsible for observing creations, updates and deletions of items of that
|
||||
* type and notifying the Search API of them by calling
|
||||
* search_api_track_item_insert(), search_api_track_item_change() and
|
||||
* search_api_track_item_delete(), as appropriate.
|
||||
* The only other restriction for item types is that they have to have a single
|
||||
* item ID field, with a scalar value. This is, e.g., used to track indexed
|
||||
* items.
|
||||
*
|
||||
* Note, however, that you can also define item types where some of these
|
||||
* conditions are not met, as long as you are aware that some functionality of
|
||||
* the Search API and related modules might then not be available for that type.
|
||||
*
|
||||
* @return array
|
||||
* An associative array keyed by item type identifier, and containing type
|
||||
* information arrays with at least the following keys:
|
||||
* - name: A human-readable name for the type.
|
||||
* - datasource controller: A class implementing the
|
||||
* SearchApiDataSourceControllerInterface interface which will be used as
|
||||
* the data source controller for this type.
|
||||
* Other, datasource-specific settings might also be placed here. These should
|
||||
* be specified with the data source controller in question.
|
||||
*
|
||||
* @see hook_search_api_item_type_info_alter()
|
||||
*/
|
||||
function hook_search_api_item_type_info() {
|
||||
// Copied from search_api_search_api_item_type_info().
|
||||
$types = array();
|
||||
|
||||
foreach (entity_get_property_info() as $type => $property_info) {
|
||||
if ($info = entity_get_info($type)) {
|
||||
$types[$type] = array(
|
||||
'name' => $info['label'],
|
||||
'datasource controller' => 'SearchApiEntityDataSourceController',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the item type info.
|
||||
*
|
||||
* Modules may implement this hook to alter the information that defines an
|
||||
* item type. All properties that are available in
|
||||
* hook_search_api_item_type_info() can be altered here.
|
||||
*
|
||||
* @param array $infos
|
||||
* The item type info array, keyed by type identifier.
|
||||
*
|
||||
* @see hook_search_api_item_type_info()
|
||||
*/
|
||||
function hook_search_api_item_type_info_alter(array &$infos) {
|
||||
// Adds a boolean value is_entity to all type options telling whether the item
|
||||
// type represents an entity type.
|
||||
foreach ($infos as $type => $info) {
|
||||
$info['is_entity'] = (bool) entity_get_info($type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define new data types for indexed properties.
|
||||
*
|
||||
* New data types will appear as new option for the „Type“ field on indexes'
|
||||
* „Fields“ tabs. Whether choosing a custom data type will have any effect
|
||||
* depends on the server on which the data is indexed.
|
||||
*
|
||||
* @return array
|
||||
* An array containing custom data type definitions, keyed by their type
|
||||
* identifier and containing the following keys:
|
||||
* - name: The human-readable name of the type.
|
||||
* - fallback: (optional) One of the default data types (the keys from
|
||||
* search_api_default_field_types()) which should be used as a fallback if
|
||||
* the server doesn't support this data type. Defaults to "string".
|
||||
* - conversion callback: (optional) If specified, a callback function for
|
||||
* converting raw values to the given type, if possible. For the contract
|
||||
* of such a callback, see example_data_type_conversion().
|
||||
*
|
||||
* @see hook_search_api_data_type_info_alter()
|
||||
* @see search_api_get_data_type_info()
|
||||
* @see example_data_type_conversion()
|
||||
*/
|
||||
function hook_search_api_data_type_info() {
|
||||
return array(
|
||||
'example_type' => array(
|
||||
'name' => t('Example type'),
|
||||
// Could be omitted, as "string" is the default.
|
||||
'fallback' => 'string',
|
||||
'conversion callback' => 'example_data_type_conversion',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter the data type info.
|
||||
*
|
||||
* Modules may implement this hook to alter the information that defines a data
|
||||
* type, or to add/remove some entirely. All properties that are available in
|
||||
* hook_search_api_data_type_info() can be altered here.
|
||||
*
|
||||
* @param array $infos
|
||||
* The data type info array, keyed by type identifier.
|
||||
*
|
||||
* @see hook_search_api_data_type_info()
|
||||
*/
|
||||
function hook_search_api_data_type_info_alter(array &$infos) {
|
||||
$infos['example_type']['name'] .= ' 2';
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers one or more callbacks that can be called at index time to add
|
||||
* additional data to the indexed items (e.g. comments or attachments to nodes),
|
||||
* alter the data in other forms or remove items from the array.
|
||||
*
|
||||
* Data-alter callbacks (which are called "Data alterations" in the UI) are
|
||||
* classes implementing the SearchApiAlterCallbackInterface interface.
|
||||
*
|
||||
* @see SearchApiAlterCallbackInterface
|
||||
*
|
||||
* @return array
|
||||
* An associative array keyed by the callback IDs and containing arrays with
|
||||
* the following keys:
|
||||
* - name: The name to display for this callback.
|
||||
* - description: A short description of what the callback does.
|
||||
* - class: The callback class.
|
||||
* - weight: (optional) Defines the order in which callbacks are displayed
|
||||
* (and, therefore, invoked) by default. Defaults to 0.
|
||||
*/
|
||||
function hook_search_api_alter_callback_info() {
|
||||
$callbacks['example_random_alter'] = array(
|
||||
'name' => t('Random alteration'),
|
||||
'description' => t('Alters all passed item data completely randomly.'),
|
||||
'class' => 'ExampleRandomAlter',
|
||||
'weight' => 100,
|
||||
);
|
||||
$callbacks['example_add_comments'] = array(
|
||||
'name' => t('Add comments'),
|
||||
'description' => t('For nodes and similar entities, adds comments.'),
|
||||
'class' => 'ExampleAddComments',
|
||||
);
|
||||
|
||||
return $callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers one or more processors. These are classes implementing the
|
||||
* SearchApiProcessorInterface interface which can be used at index and search
|
||||
* time to pre-process item data or the search query, and at search time to
|
||||
* post-process the returned search results.
|
||||
*
|
||||
* @see SearchApiProcessorInterface
|
||||
*
|
||||
* @return array
|
||||
* An associative array keyed by the processor id and containing arrays
|
||||
* with the following keys:
|
||||
* - name: The name to display for this processor.
|
||||
* - description: A short description of what the processor does at each
|
||||
* phase.
|
||||
* - class: The processor class, which has to implement the
|
||||
* SearchApiProcessorInterface interface.
|
||||
* - weight: (optional) Defines the order in which processors are displayed
|
||||
* (and, therefore, invoked) by default. Defaults to 0.
|
||||
*/
|
||||
function hook_search_api_processor_info() {
|
||||
$callbacks['example_processor'] = array(
|
||||
'name' => t('Example processor'),
|
||||
'description' => t('Pre- and post-processes data in really cool ways.'),
|
||||
'class' => 'ExampleSearchApiProcessor',
|
||||
'weight' => -1,
|
||||
);
|
||||
$callbacks['example_processor_minimal'] = array(
|
||||
'name' => t('Example processor 2'),
|
||||
'description' => t('Processor with minimal description.'),
|
||||
'class' => 'ExampleSearchApiProcessor2',
|
||||
);
|
||||
|
||||
return $callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 data alterations, 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 array $items
|
||||
* The entities that will be indexed (before calling any data alterations).
|
||||
* @param SearchApiIndex $index
|
||||
* The search index on which items will be indexed.
|
||||
*/
|
||||
function hook_search_api_index_items_alter(array &$items, SearchApiIndex $index) {
|
||||
foreach ($items as $id => $item) {
|
||||
if ($id % 5 == 0) {
|
||||
unset($items[$id]);
|
||||
}
|
||||
}
|
||||
example_store_indexed_entity_ids($index->item_type, array_keys($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets modules alter a search query before executing it.
|
||||
*
|
||||
* @param SearchApiQueryInterface $query
|
||||
* The SearchApiQueryInterface object representing the search query.
|
||||
*/
|
||||
function hook_search_api_query_alter(SearchApiQueryInterface $query) {
|
||||
$info = entity_get_info($index->item_type);
|
||||
$query->condition($info['entity keys']['id'], 0, '!=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on search servers when they are loaded.
|
||||
*
|
||||
* @param array $servers
|
||||
* An array of loaded SearchApiServer objects.
|
||||
*/
|
||||
function hook_search_api_server_load(array $servers) {
|
||||
foreach ($servers as $server) {
|
||||
db_insert('example_search_server_access')
|
||||
->fields(array(
|
||||
'server' => $server->machine_name,
|
||||
'access_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new search server was created.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The new server.
|
||||
*/
|
||||
function hook_search_api_server_insert(SearchApiServer $server) {
|
||||
db_insert('example_search_server')
|
||||
->fields(array(
|
||||
'server' => $server->machine_name,
|
||||
'insert_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* A search server was edited, enabled or disabled.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The edited server.
|
||||
*/
|
||||
function hook_search_api_server_update(SearchApiServer $server) {
|
||||
if ($server->name != $server->original->name) {
|
||||
db_insert('example_search_server_name_update')
|
||||
->fields(array(
|
||||
'server' => $server->machine_name,
|
||||
'update_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A search server was deleted.
|
||||
*
|
||||
* @param SearchApiServer $server
|
||||
* The deleted server.
|
||||
*/
|
||||
function hook_search_api_server_delete(SearchApiServer $server) {
|
||||
db_insert('example_search_server_update')
|
||||
->fields(array(
|
||||
'server' => $server->machine_name,
|
||||
'update_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
db_delete('example_search_server')
|
||||
->condition('server', $server->machine_name)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define default search servers.
|
||||
*
|
||||
* @return array
|
||||
* An array of default search servers, keyed by machine names.
|
||||
*
|
||||
* @see hook_default_search_api_server_alter()
|
||||
*/
|
||||
function hook_default_search_api_server() {
|
||||
$defaults['main'] = entity_create('search_api_server', array(
|
||||
'name' => 'Main server',
|
||||
'machine_name' => 'main',// Must be same as the used array key.
|
||||
// Other properties ...
|
||||
));
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter default search servers.
|
||||
*
|
||||
* @param array $defaults
|
||||
* An array of default search servers, keyed by machine names.
|
||||
*
|
||||
* @see hook_default_search_api_server()
|
||||
*/
|
||||
function hook_default_search_api_server_alter(array &$defaults) {
|
||||
$defaults['main']->name = 'Customized main server';
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on search indexes when they are loaded.
|
||||
*
|
||||
* @param array $indexes
|
||||
* An array of loaded SearchApiIndex objects.
|
||||
*/
|
||||
function hook_search_api_index_load(array $indexes) {
|
||||
foreach ($indexes as $index) {
|
||||
db_insert('example_search_index_access')
|
||||
->fields(array(
|
||||
'index' => $index->machine_name,
|
||||
'access_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new search index was created.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The new index.
|
||||
*/
|
||||
function hook_search_api_index_insert(SearchApiIndex $index) {
|
||||
db_insert('example_search_index')
|
||||
->fields(array(
|
||||
'index' => $index->machine_name,
|
||||
'insert_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* A search index was edited in any way.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The edited index.
|
||||
*/
|
||||
function hook_search_api_index_update(SearchApiIndex $index) {
|
||||
if ($index->name != $index->original->name) {
|
||||
db_insert('example_search_index_name_update')
|
||||
->fields(array(
|
||||
'index' => $index->machine_name,
|
||||
'update_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A search index was scheduled for reindexing
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The edited index.
|
||||
* @param $clear
|
||||
* Boolean indicating whether the index was also cleared.
|
||||
*/
|
||||
function hook_search_api_index_reindex(SearchApiIndex $index, $clear = FALSE) {
|
||||
db_insert('example_search_index_reindexed')
|
||||
->fields(array(
|
||||
'index' => $index->id,
|
||||
'update_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* A search index was deleted.
|
||||
*
|
||||
* @param SearchApiIndex $index
|
||||
* The deleted index.
|
||||
*/
|
||||
function hook_search_api_index_delete(SearchApiIndex $index) {
|
||||
db_insert('example_search_index_update')
|
||||
->fields(array(
|
||||
'index' => $index->machine_name,
|
||||
'update_time' => REQUEST_TIME,
|
||||
))
|
||||
->execute();
|
||||
db_delete('example_search_index')
|
||||
->condition('index', $index->machine_name)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define default search indexes.
|
||||
*
|
||||
* @return array
|
||||
* An array of default search indexes, keyed by machine names.
|
||||
*
|
||||
* @see hook_default_search_api_index_alter()
|
||||
*/
|
||||
function hook_default_search_api_index() {
|
||||
$defaults['main'] = entity_create('search_api_index', array(
|
||||
'name' => 'Main index',
|
||||
'machine_name' => 'main',// Must be same as the used array key.
|
||||
// Other properties ...
|
||||
));
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter default search indexes.
|
||||
*
|
||||
* @param array $defaults
|
||||
* An array of default search indexes, keyed by machine names.
|
||||
*
|
||||
* @see hook_default_search_api_index()
|
||||
*/
|
||||
function hook_default_search_api_index_alter(array &$defaults) {
|
||||
$defaults['main']->name = 'Customized main index';
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a raw value from an entity to a custom data type.
|
||||
*
|
||||
* This function will be called for fields of the specific data type to convert
|
||||
* all individual values of the field to the correct format.
|
||||
*
|
||||
* @param $value
|
||||
* The raw, single value, as extracted from an entity wrapper.
|
||||
* @param $original_type
|
||||
* The original Entity API type of the value.
|
||||
* @param $type
|
||||
* The custom data type to which the value should be converted. Can be ignored
|
||||
* if the callback is only used for a single data type.
|
||||
*
|
||||
* @return
|
||||
* The converted value, if a conversion could be executed. NULL otherwise.
|
||||
*
|
||||
* @see hook_search_api_data_type_info()
|
||||
*/
|
||||
function example_data_type_conversion($value, $original_type, $type) {
|
||||
if ($type === 'example_type') {
|
||||
// The example_type type apparently requires a rather complex data format.
|
||||
return array(
|
||||
'value' => $value,
|
||||
'original' => $original_type,
|
||||
);
|
||||
}
|
||||
// Someone used this callback for another, unknown type. Return NULL.
|
||||
// (Normally, you can just assume that the/a correct type is given.)
|
||||
return NULL;
|
||||
}
|
311
search_api.drush.inc
Normal file
311
search_api.drush.inc
Normal file
@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Drush commands for SearchAPI.
|
||||
*
|
||||
* Original file by agentrickard for Palantir.net
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_drush_command().
|
||||
*/
|
||||
function search_api_drush_command() {
|
||||
$items = array();
|
||||
|
||||
$items['search-api-list'] = array(
|
||||
'description' => 'List all search indexes.',
|
||||
'examples' => array(
|
||||
'drush searchapi-list' => dt('List all search indexes.'),
|
||||
'drush sapi-l' => dt('Alias to list all search indexes.'),
|
||||
),
|
||||
'aliases' => array('sapi-l'),
|
||||
);
|
||||
|
||||
$items['search-api-status'] = array(
|
||||
'description' => 'Show the status of one or all search indexes.',
|
||||
'examples' => array(
|
||||
'drush searchapi-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 1' => dt('Show the status of the search index with the ID !id.', array('!id' => 1)),
|
||||
'drush sapi-s default_node_index' => dt('Show the status of the search index with the machine name !name.', array('!name' => 'default_node_index')),
|
||||
),
|
||||
'arguments' => array(
|
||||
'index_id' => dt('The numeric ID or machine name of an index.'),
|
||||
),
|
||||
'aliases' => array('sapi-s'),
|
||||
);
|
||||
|
||||
$items['search-api-index'] = array(
|
||||
'description' => 'Index items for one or all enabled search_api indexes.',
|
||||
'examples' => array(
|
||||
'drush searchapi-index' => dt('Index items for all enabled indexes.'),
|
||||
'drush sapi-i' => dt('Alias to index items for all enabled indexes.'),
|
||||
'drush sapi-i 1' => dt('Index items for the index with the ID !id.', array('!id' => 1)),
|
||||
'drush sapi-i default_node_index' => dt('Index items for the index with the machine name !name.', array('!name' => 'default_node_index')),
|
||||
'drush sapi-i 1 100' => dt("Index a maximum number of !limit items (index's cron batch size items per batch run) for the index with the ID !id.", array('!limit' => 100, '!id' => 1)),
|
||||
'drush sapi-i 1 100 10' => dt("Index a maximum number of !limit items (!batch_size items per batch run) for the index with the ID !id.", array('!limit' => 100, '!batch_size' => 10, '!id' => 1)),
|
||||
),
|
||||
'arguments' => array(
|
||||
'index_id' => dt('The numeric ID or machine name of an index.'),
|
||||
'limit' => dt("The number of items to index (index's cron batch size items per run). 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 index's cron batch size."),
|
||||
),
|
||||
'aliases' => array('sapi-i'),
|
||||
);
|
||||
|
||||
$items['search-api-reindex'] = array(
|
||||
'description' => 'Force reindexing of one or all search indexes, without clearing existing index data.',
|
||||
'examples' => array(
|
||||
'drush searchapi-reindex' => dt('Schedule all search indexes for reindexing.'),
|
||||
'drush sapi-r' => dt('Alias to schedule all search indexes for reindexing .'),
|
||||
'drush sapi-r 1' => dt('Schedule the search index with the ID !id for re-indexing.', array('!id' => 1)),
|
||||
'drush sapi-r default_node_index' => dt('Schedule the search index with the machine name !name for re-indexing.', array('!name' => 'default_node_index')),
|
||||
),
|
||||
'arguments' => array(
|
||||
'index_id' => dt('The numeric ID or machine name of an index.'),
|
||||
),
|
||||
'aliases' => array('sapi-r'),
|
||||
);
|
||||
|
||||
$items['search-api-clear'] = array(
|
||||
'description' => 'Clear one or all search indexes and mark them for re-indexing.',
|
||||
'examples' => array(
|
||||
'drush searchapi-clear' => dt('Clear all search indexes.'),
|
||||
'drush sapi-c' => dt('Alias to clear all search indexes.'),
|
||||
'drush sapi-r 1' => dt('Clear the search index with the ID !id.', array('!id' => 1)),
|
||||
'drush sapi-r default_node_index' => dt('Clear the search index with the machine name !name.', array('!name' => 'default_node_index')),
|
||||
),
|
||||
'arguments' => array(
|
||||
'index_id' => dt('The numeric ID or machine name of an index.'),
|
||||
),
|
||||
'aliases' => array('sapi-c'),
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* List all search indexes.
|
||||
*/
|
||||
function drush_search_api_list() {
|
||||
if (search_api_drush_static(__FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
// See search_api_list_indexes()
|
||||
$indexes = search_api_index_load_multiple(FALSE);
|
||||
if (empty($indexes)) {
|
||||
drush_print(dt('There are no indexes present.'));
|
||||
return;
|
||||
}
|
||||
$rows[] = array(
|
||||
dt('Id'),
|
||||
dt('Name'),
|
||||
dt('Index'),
|
||||
dt('Server'),
|
||||
dt('Type'),
|
||||
dt('Status'),
|
||||
dt('Limit'),
|
||||
);
|
||||
foreach ($indexes as $index) {
|
||||
$type = search_api_get_item_type_info($index->item_type);
|
||||
$type = isset($type['name']) ? $type['name'] : $index->item_type;
|
||||
$server = $index->server();
|
||||
$server = $server ? $server->name : '(' . t('none') . ')';
|
||||
$row = array(
|
||||
$index->id,
|
||||
$index->name,
|
||||
$index->machine_name,
|
||||
$server,
|
||||
$type,
|
||||
$index->enabled ? t('enabled') : t('disabled'),
|
||||
$index->options['cron_limit'],
|
||||
);
|
||||
$rows[] = $row;
|
||||
}
|
||||
drush_print_table($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display index status.
|
||||
*/
|
||||
function drush_search_api_status($index_id = NULL) {
|
||||
if (search_api_drush_static(__FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
$indexes = search_api_drush_get_index($index_id);
|
||||
if (empty($indexes)) {
|
||||
return;
|
||||
}
|
||||
// See search_api_index_status()
|
||||
$rows = array(array(
|
||||
dt('Id'),
|
||||
dt('Index'),
|
||||
dt('% Complete'),
|
||||
dt('Indexed'),
|
||||
dt('Total'),
|
||||
));
|
||||
foreach ($indexes as $index) {
|
||||
$status = search_api_index_status($index);
|
||||
$complete = ($status['total'] > 0) ? 100 * round($status['indexed'] / $status['total'], 3) . '%' : '-';
|
||||
$row = array(
|
||||
$index->id,
|
||||
$index->name,
|
||||
$complete,
|
||||
$status['indexed'],
|
||||
$status['total'],
|
||||
);
|
||||
$rows[] = $row;
|
||||
}
|
||||
drush_print_table($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index items.
|
||||
*
|
||||
* @param string|integer $index_id
|
||||
* The index name or id for which items should be indexed.
|
||||
* @param integer $limit
|
||||
* Maximum number of items to index.
|
||||
* @param integer $batch_size
|
||||
* Number of items to index per batch.
|
||||
*/
|
||||
function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = NULL) {
|
||||
if (search_api_drush_static(__FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
$indexes = search_api_drush_get_index($index_id);
|
||||
if (empty($indexes)) {
|
||||
return;
|
||||
}
|
||||
foreach ($indexes as $index) {
|
||||
// Get the number of remaing items to index.
|
||||
$datasource = $index->datasource();
|
||||
$index_status = $datasource->getIndexStatus($index);
|
||||
$remaining = $index_status['total'] - $index_status['indexed'];
|
||||
|
||||
// Get the number of items to index per batch run.
|
||||
if (!isset($batch_size)) {
|
||||
$batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit'];
|
||||
}
|
||||
elseif ($batch_size <= 0) {
|
||||
$batch_size = $remaining;
|
||||
}
|
||||
|
||||
// Get the number items to index.
|
||||
if (!isset($limit) || !is_int($limit += 0) || $limit <= 0) {
|
||||
$limit = $remaining;
|
||||
}
|
||||
|
||||
drush_log(dt("Indexing a maximum number of !limit items (!batch_size items per batch run) for the index !index.", array('!index' => $index->name, '!limit' => $limit, '!batch_size' => $batch_size)), 'ok');
|
||||
|
||||
// Create the batch.
|
||||
if (!_search_api_batch_indexing_create($index, $batch_size, $limit, $remaining, TRUE)) {
|
||||
drush_log(dt("Couldn't create a batch, please check the batch size and limit parameters."), 'error');
|
||||
}
|
||||
else {
|
||||
// Launch the batch process.
|
||||
drush_backend_batch_process();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of formal_plural that works with drush as 't' may not be available.
|
||||
*/
|
||||
function _search_api_drush_format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
|
||||
$args['@count'] = $count;
|
||||
if ($count == 1) {
|
||||
return dt($singular, $args, $options);
|
||||
}
|
||||
|
||||
// Get the plural index through the gettext formula.
|
||||
$index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
|
||||
// If the index cannot be computed, use the plural as a fallback (which
|
||||
// allows for most flexiblity with the replaceable @count value).
|
||||
if ($index < 0) {
|
||||
return dt($plural, $args, $options);
|
||||
}
|
||||
else {
|
||||
switch ($index) {
|
||||
case "0":
|
||||
return dt($singular, $args, $options);
|
||||
case "1":
|
||||
return dt($plural, $args, $options);
|
||||
default:
|
||||
unset($args['@count']);
|
||||
$args['@count[' . $index . ']'] = $count;
|
||||
return dt(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark for re-indexing.
|
||||
*/
|
||||
function drush_search_api_reindex($index_id = NULL) {
|
||||
if (search_api_drush_static(__FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
$indexes = search_api_drush_get_index($index_id);
|
||||
if (empty($indexes)) {
|
||||
return;
|
||||
}
|
||||
// See search_api_index_reindex()
|
||||
foreach ($indexes as $index) {
|
||||
$index->reindex();
|
||||
drush_log(dt('!index was successfully marked for re-indexing.', array('!index' => $index->machine_name)), 'ok');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear an index.
|
||||
*/
|
||||
function drush_search_api_clear($index_id = NULL) {
|
||||
if (search_api_drush_static(__FUNCTION__)) {
|
||||
return;
|
||||
}
|
||||
$indexes = search_api_drush_get_index($index_id);
|
||||
if (empty($indexes)) {
|
||||
return;
|
||||
}
|
||||
// See search_api_index_reindex()
|
||||
foreach ($indexes as $index) {
|
||||
$index->clear();
|
||||
drush_log(dt('!index was successfully cleared.', array('!index' => $index->machine_name)), 'ok');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to return an index or all indexes as an array.
|
||||
*
|
||||
* @param $index_id
|
||||
* (optional) The provided index id.
|
||||
*
|
||||
* @return
|
||||
* An array of indexes.
|
||||
*/
|
||||
function search_api_drush_get_index($index_id = NULL) {
|
||||
$ids = isset($index_id) ? array($index_id) : FALSE;
|
||||
$indexes = search_api_index_load_multiple($ids);
|
||||
if (empty($indexes)) {
|
||||
drush_set_error(dt('Invalid index_id or no indexes present. Listing all indexes:'));
|
||||
drush_print();
|
||||
drush_search_api_list();
|
||||
}
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static lookup to prevent Drush 4 from running twice.
|
||||
*
|
||||
* @see http://drupal.org/node/704848
|
||||
*/
|
||||
function search_api_drush_static($function) {
|
||||
static $index = array();
|
||||
if (isset($index[$function])) {
|
||||
return TRUE;
|
||||
}
|
||||
$index[$function] = TRUE;
|
||||
}
|
38
search_api.info
Normal file
38
search_api.info
Normal file
@ -0,0 +1,38 @@
|
||||
name = Search API
|
||||
description = "Provides a generic API for modules offering search capabilites."
|
||||
dependencies[] = entity
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
files[] = search_api.test
|
||||
files[] = includes/callback.inc
|
||||
files[] = includes/callback_add_aggregation.inc
|
||||
files[] = includes/callback_add_hierarchy.inc
|
||||
files[] = includes/callback_add_url.inc
|
||||
files[] = includes/callback_add_viewed_entity.inc
|
||||
files[] = includes/callback_bundle_filter.inc
|
||||
files[] = includes/callback_language_control.inc
|
||||
files[] = includes/callback_node_access.inc
|
||||
files[] = includes/callback_node_status.inc
|
||||
files[] = includes/datasource.inc
|
||||
files[] = includes/datasource_entity.inc
|
||||
files[] = includes/datasource_external.inc
|
||||
files[] = includes/exception.inc
|
||||
files[] = includes/index_entity.inc
|
||||
files[] = includes/processor.inc
|
||||
files[] = includes/processor_html_filter.inc
|
||||
files[] = includes/processor_ignore_case.inc
|
||||
files[] = includes/processor_stopwords.inc
|
||||
files[] = includes/processor_tokenizer.inc
|
||||
files[] = includes/query.inc
|
||||
files[] = includes/server_entity.inc
|
||||
files[] = includes/service.inc
|
||||
|
||||
configure = admin/config/search/search_api
|
||||
|
||||
; Information added by drupal.org packaging script on 2013-01-09
|
||||
version = "7.x-1.4"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1357726719"
|
||||
|
808
search_api.install
Normal file
808
search_api.install
Normal file
@ -0,0 +1,808 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the Search API module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function search_api_schema() {
|
||||
$schema['search_api_server'] = array(
|
||||
'description' => 'Stores all search servers created through the Search API.',
|
||||
'fields' => array(
|
||||
'id' => array(
|
||||
'description' => 'The primary identifier for a server.',
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => 'The displayed name for a server.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'machine_name' => array(
|
||||
'description' => 'The machine name for a server.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'description' => array(
|
||||
'description' => 'The displayed description for a server.',
|
||||
'type' => 'text',
|
||||
'not null' => FALSE,
|
||||
),
|
||||
'class' => array(
|
||||
'description' => 'The id of the service class to use for this server.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'options' => array(
|
||||
'description' => 'The options used to configure the service object.',
|
||||
'type' => 'text',
|
||||
'size' => 'medium',
|
||||
'serialize' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'enabled' => array(
|
||||
'description' => 'A flag indicating whether the server is enabled.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
),
|
||||
'status' => array(
|
||||
'description' => 'The exportable status of the entity.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0x01,
|
||||
'size' => 'tiny',
|
||||
),
|
||||
'module' => array(
|
||||
'description' => 'The name of the providing module if the entity has been defined in code.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => FALSE,
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'enabled' => array('enabled'),
|
||||
),
|
||||
'unique keys' => array(
|
||||
'machine_name' => array('machine_name'),
|
||||
),
|
||||
'primary key' => array('id'),
|
||||
);
|
||||
|
||||
$schema['search_api_index'] = array(
|
||||
'description' => 'Stores all search indexes on a {search_api_server}.',
|
||||
'fields' => array(
|
||||
'id' => array(
|
||||
'description' => 'An integer identifying the index.',
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => 'A name to be displayed for the index.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'machine_name' => array(
|
||||
'description' => 'The machine name of the index.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'description' => array(
|
||||
'description' => "A string describing the index' use to users.",
|
||||
'type' => 'text',
|
||||
'not null' => FALSE,
|
||||
),
|
||||
'server' => array(
|
||||
'description' => 'The {search_api_server}.machine_name with which data should be indexed.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => FALSE,
|
||||
),
|
||||
'item_type' => array(
|
||||
'description' => 'The type of items stored in this index.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'options' => array(
|
||||
'description' => 'An array of additional arguments configuring this index.',
|
||||
'type' => 'text',
|
||||
'size' => 'medium',
|
||||
'serialize' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'enabled' => array(
|
||||
'description' => 'A flag indicating whether this index is enabled.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
),
|
||||
'read_only' => array(
|
||||
'description' => 'A flag indicating whether to write to this index.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'status' => array(
|
||||
'description' => 'The exportable status of the entity.',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0x01,
|
||||
'size' => 'tiny',
|
||||
),
|
||||
'module' => array(
|
||||
'description' => 'The name of the providing module if the entity has been defined in code.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => FALSE,
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'item_type' => array('item_type'),
|
||||
'server' => array('server'),
|
||||
'enabled' => array('enabled'),
|
||||
),
|
||||
'unique keys' => array(
|
||||
'machine_name' => array('machine_name'),
|
||||
),
|
||||
'primary key' => array('id'),
|
||||
);
|
||||
|
||||
$schema['search_api_item'] = array(
|
||||
'description' => 'Stores the items which should be indexed for each index, and their status.',
|
||||
'fields' => array(
|
||||
'item_id' => array(
|
||||
'description' => "The item's entity id (e.g. {node}.nid for nodes).",
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'index_id' => array(
|
||||
'description' => 'The {search_api_index}.id this item belongs to.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'changed' => array(
|
||||
'description' => 'Either a flag or a timestamp to indicate if or when the item was changed since it was last indexed.',
|
||||
'type' => 'int',
|
||||
'size' => 'big',
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'indexing' => array('index_id', 'changed'),
|
||||
),
|
||||
'primary key' => array('item_id', 'index_id'),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*
|
||||
* Creates a default node index if the module is installed manually.
|
||||
*/
|
||||
function search_api_install() {
|
||||
// In case the module is installed via an installation profile, a batch is
|
||||
// active and we skip that.
|
||||
if (batch_get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = t('Default node index');
|
||||
$values = array(
|
||||
'name' => $name,
|
||||
'machine_name' => preg_replace('/[^a-z0-9]+/', '_', drupal_strtolower($name)),
|
||||
'description' => t('An automatically created search index for indexing node data. Might be configured to specific needs.'),
|
||||
'server' => NULL,
|
||||
'item_type' => 'node',
|
||||
'options' => array(
|
||||
'cron_limit' => '50',
|
||||
'data_alter_callbacks' => array(
|
||||
'search_api_alter_node_access' => array(
|
||||
'status' => 1,
|
||||
'weight' => '0',
|
||||
'settings' => array(),
|
||||
),
|
||||
),
|
||||
'processors' => array(
|
||||
'search_api_case_ignore' => array(
|
||||
'status' => 1,
|
||||
'weight' => '0',
|
||||
'settings' => array(
|
||||
'strings' => 0,
|
||||
),
|
||||
),
|
||||
'search_api_html_filter' => array(
|
||||
'status' => 1,
|
||||
'weight' => '10',
|
||||
'settings' => array(
|
||||
'title' => 0,
|
||||
'alt' => 1,
|
||||
'tags' => "h1 = 5\n" .
|
||||
"h2 = 3\n" .
|
||||
"h3 = 2\n" .
|
||||
"strong = 2\n" .
|
||||
"b = 2\n" .
|
||||
"em = 1.5\n" .
|
||||
"u = 1.5",
|
||||
),
|
||||
),
|
||||
'search_api_tokenizer' => array(
|
||||
'status' => 1,
|
||||
'weight' => '20',
|
||||
'settings' => array(
|
||||
'spaces' => '[^\\p{L}\\p{N}]',
|
||||
'ignorable' => '[-]',
|
||||
),
|
||||
),
|
||||
),
|
||||
'fields' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'text',
|
||||
'boost' => '5.0',
|
||||
),
|
||||
'promote' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'sticky' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'created' => array(
|
||||
'type' => 'date',
|
||||
),
|
||||
'changed' => array(
|
||||
'type' => 'date',
|
||||
),
|
||||
'author' => array(
|
||||
'type' => 'integer',
|
||||
'entity_type' => 'user',
|
||||
),
|
||||
'comment_count' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
'search_api_language' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'body:value' => array(
|
||||
'type' => 'text',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
search_api_index_insert($values);
|
||||
drupal_set_message('The Search API module was installed. A new default node index was created.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_enable().
|
||||
*
|
||||
* Mark all items as "dirty", since we can't know whether they are.
|
||||
*/
|
||||
function search_api_enable() {
|
||||
$types = array();
|
||||
foreach (search_api_index_load_multiple(FALSE) as $index) {
|
||||
if ($index->enabled) {
|
||||
$types[$index->item_type][] = $index;
|
||||
}
|
||||
}
|
||||
foreach ($types as $type => $indexes) {
|
||||
$controller = search_api_get_datasource_controller($type);
|
||||
$controller->startTracking($indexes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_disable().
|
||||
*/
|
||||
function search_api_disable() {
|
||||
$types = array();
|
||||
foreach (search_api_index_load_multiple(FALSE) as $index) {
|
||||
$types[$index->item_type][] = $index;
|
||||
}
|
||||
foreach ($types as $type => $indexes) {
|
||||
$controller = search_api_get_datasource_controller($type);
|
||||
$controller->stopTracking($indexes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function search_api_uninstall() {
|
||||
variable_del('search_api_tasks');
|
||||
variable_del('search_api_index_worker_callback_runtime');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update function that adds the machine names for servers and indexes.
|
||||
*/
|
||||
function search_api_update_7101() {
|
||||
$tx = db_transaction();
|
||||
try {
|
||||
// Servers
|
||||
$spec = array(
|
||||
'description' => 'The machine name for a server.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
);
|
||||
db_add_field('search_api_server', 'machine_name', $spec);
|
||||
|
||||
$names = array();
|
||||
$servers = db_select('search_api_server', 's')
|
||||
->fields('s')
|
||||
->execute();
|
||||
foreach ($servers as $server) {
|
||||
$base = $name = drupal_strtolower(preg_replace('/[^a-z0-9]+/i', '_', $server->name));
|
||||
$i = 0;
|
||||
while (isset($names[$name])) {
|
||||
$name = $base . '_' . ++$i;
|
||||
}
|
||||
$names[$name] = TRUE;
|
||||
db_update('search_api_server')
|
||||
->fields(array('machine_name' => $name))
|
||||
->condition('id', $server->id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
db_add_unique_key('search_api_server', 'machine_name', array('machine_name'));
|
||||
|
||||
//Indexes
|
||||
$spec = array(
|
||||
'description' => 'The machine name of the index.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
);
|
||||
db_add_field('search_api_index', 'machine_name', $spec);
|
||||
|
||||
$names = array();
|
||||
$indexes = db_select('search_api_index', 'i')
|
||||
->fields('i')
|
||||
->execute();
|
||||
foreach ($indexes as $index) {
|
||||
$base = $name = drupal_strtolower(preg_replace('/[^a-z0-9]+/i', '_', $index->name));
|
||||
$i = 0;
|
||||
while (isset($names[$name])) {
|
||||
$name = $base . '_' . ++$i;
|
||||
}
|
||||
$names[$name] = TRUE;
|
||||
db_update('search_api_index')
|
||||
->fields(array('machine_name' => $name))
|
||||
->condition('id', $index->id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
db_add_unique_key('search_api_index', 'machine_name', array('machine_name'));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$tx->rollback();
|
||||
try {
|
||||
db_drop_field('search_api_server', 'machine_name');
|
||||
db_drop_field('search_api_index', 'machine_name');
|
||||
}
|
||||
catch (Exception $e1) {
|
||||
// Ignore.
|
||||
}
|
||||
throw new DrupalUpdateException(t('An exception occurred during the update: @msg.', array('@msg' => $e->getMessage())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update replacing IDs with machine names for foreign keys.
|
||||
* {search_api_index}.server and {search_api_item}.index_id are altered.
|
||||
*/
|
||||
function search_api_update_7102() {
|
||||
// Update of search_api_index:
|
||||
$indexes = array();
|
||||
$select = db_select('search_api_index', 'i')->fields('i');
|
||||
foreach ($select->execute() as $index) {
|
||||
$indexes[$index->id] = $index;
|
||||
}
|
||||
$servers = db_select('search_api_server', 's')->fields('s', array('id', 'machine_name'))->execute()->fetchAllKeyed();
|
||||
|
||||
db_drop_index('search_api_index', 'server');
|
||||
db_drop_field('search_api_index', 'server');
|
||||
$spec = array(
|
||||
'description' => 'The {search_api_server}.machine_name with which data should be indexed.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => FALSE,
|
||||
);
|
||||
db_add_field('search_api_index', 'server', $spec);
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
db_update('search_api_index')
|
||||
->fields(array('server' => $servers[$index->server]))
|
||||
->condition('id', $index->id)
|
||||
->execute();
|
||||
}
|
||||
db_add_index('search_api_index', 'server', array('server'));
|
||||
|
||||
// Update of search_api_item:
|
||||
db_drop_index('search_api_item', 'indexing');
|
||||
db_drop_primary_key('search_api_item');
|
||||
$spec = array(
|
||||
'description' => 'The {search_api_index}.machine_name this item belongs to.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
);
|
||||
$keys_new = array(
|
||||
'indexes' => array(
|
||||
'indexing' => array('index_id', 'changed'),
|
||||
),
|
||||
'primary key' => array('item_id', 'index_id'),
|
||||
);
|
||||
db_change_field('search_api_item', 'index_id', 'index_id', $spec, $keys_new);
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
// We explicitly forbid numeric machine names, therefore we don't have to
|
||||
// worry about conflicts here.
|
||||
db_update('search_api_item')
|
||||
->fields(array(
|
||||
'index_id' => $index->machine_name,
|
||||
))
|
||||
->condition('index_id', $index->id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the database fields newly required for entities by the Entity API.
|
||||
*/
|
||||
function search_api_update_7103() {
|
||||
if (!function_exists('entity_exportable_schema_fields')) {
|
||||
throw new DrupalUpdateException(t('Please update the Entity API module first.'));
|
||||
}
|
||||
foreach (array('search_api_server', 'search_api_index') as $table) {
|
||||
foreach (entity_exportable_schema_fields() as $field => $spec) {
|
||||
db_add_field($table, $field, $spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the "Fields to run on" settings for processors.
|
||||
*/
|
||||
function search_api_update_7107() {
|
||||
$rows = db_select('search_api_index', 'i')
|
||||
->fields('i', array('id', 'options'))
|
||||
->execute()
|
||||
->fetchAllKeyed();
|
||||
foreach ($rows as $id => $options) {
|
||||
$opt = unserialize($options);
|
||||
$processors = &$opt['processors'];
|
||||
// Only update our own processors, don't mess with others.
|
||||
$check_processors = array(
|
||||
'search_api_case_ignore' => 1,
|
||||
'search_api_html_filter' => 1,
|
||||
'search_api_tokenizer' => 1,
|
||||
);
|
||||
foreach (array_intersect_key($processors, $check_processors) as $name => $info) {
|
||||
$types = array('text');
|
||||
if (!empty($info['settings']['strings'])) {
|
||||
$types[] = 'string';
|
||||
unset($processors[$name]['settings']['strings']);
|
||||
}
|
||||
foreach ($opt['fields'] as $field => $info) {
|
||||
if ($info['indexed'] && search_api_is_text_type($info['type'], $types)) {
|
||||
$processors[$name]['settings']['fields'][$field] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
$opt = serialize($opt);
|
||||
if ($opt != $options) {
|
||||
db_update('search_api_index')
|
||||
->fields(array(
|
||||
'options' => $opt,
|
||||
))
|
||||
->condition('id', $id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change {search_api_item}.index_id back to the index' numeric ID.
|
||||
*/
|
||||
function search_api_update_7104() {
|
||||
$select = db_select('search_api_index', 'i')->fields('i');
|
||||
foreach ($select->execute() as $index) {
|
||||
// We explicitly forbid numeric machine names, therefore we don't have to
|
||||
// worry about conflicts here.
|
||||
db_update('search_api_item')
|
||||
->fields(array(
|
||||
'index_id' => $index->id,
|
||||
))
|
||||
->condition('index_id', $index->machine_name)
|
||||
->execute();
|
||||
}
|
||||
|
||||
// Update primary key and index.
|
||||
db_drop_index('search_api_item', 'indexing');
|
||||
db_drop_primary_key('search_api_item');
|
||||
$spec = array(
|
||||
'description' => 'The {search_api_index}.id this item belongs to.',
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
);
|
||||
$keys_new = array(
|
||||
'indexes' => array(
|
||||
'indexing' => array('index_id', 'changed'),
|
||||
),
|
||||
'primary key' => array('item_id', 'index_id'),
|
||||
);
|
||||
db_change_field('search_api_item', 'index_id', 'index_id', $spec, $keys_new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all empty aggregated fields for the search_api_alter_add_fulltext data
|
||||
* alterations.
|
||||
*/
|
||||
function search_api_update_7105() {
|
||||
$rows = db_select('search_api_index', 'i')
|
||||
->fields('i', array('id', 'options'))
|
||||
->execute()
|
||||
->fetchAllKeyed();
|
||||
foreach ($rows as $id => $options) {
|
||||
$opt = unserialize($options);
|
||||
if (isset($opt['data_alter_callbacks']['search_api_alter_add_fulltext']['settings']['fields'])) {
|
||||
foreach ($opt['data_alter_callbacks']['search_api_alter_add_fulltext']['settings']['fields'] as $name => $field) {
|
||||
if (empty($field['name']) || empty($field['fields'])) {
|
||||
unset($opt['data_alter_callbacks']['search_api_alter_add_fulltext']['settings']['fields'][$name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$opt = serialize($opt);
|
||||
if ($opt != $options) {
|
||||
db_update('search_api_index')
|
||||
->fields(array(
|
||||
'options' => $opt,
|
||||
))
|
||||
->condition('id', $id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the settings for the "Aggregated fields" data alteration.
|
||||
*/
|
||||
function search_api_update_7106() {
|
||||
$rows = db_select('search_api_index', 'i')
|
||||
->fields('i')
|
||||
->execute()
|
||||
->fetchAll();
|
||||
foreach ($rows as $row) {
|
||||
$opt = unserialize($row->options);
|
||||
$callbacks = &$opt['data_alter_callbacks'];
|
||||
if (isset($callbacks['search_api_alter_add_fulltext'])) {
|
||||
$callbacks['search_api_alter_add_aggregation'] = $callbacks['search_api_alter_add_fulltext'];
|
||||
unset($callbacks['search_api_alter_add_fulltext']);
|
||||
if (!empty($callbacks['search_api_alter_add_aggregation']['settings']['fields'])) {
|
||||
foreach ($callbacks['search_api_alter_add_aggregation']['settings']['fields'] as $field => &$info) {
|
||||
if (!isset($info['type'])) {
|
||||
$info['type'] = 'fulltext';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$opt = serialize($opt);
|
||||
if ($opt != $row->options) {
|
||||
// Mark the entity as overridden, in case it has been defined in code
|
||||
// only.
|
||||
$row->status |= 0x01;
|
||||
db_update('search_api_index')
|
||||
->fields(array(
|
||||
'options' => $opt,
|
||||
'status' => $row->status,
|
||||
))
|
||||
->condition('id', $row->id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "read only" property to Search API index entities.
|
||||
*/
|
||||
function search_api_update_7108() {
|
||||
$db_field = array(
|
||||
'description' => 'A flag indicating whether to write to this index.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
);
|
||||
db_add_field('search_api_index', 'read_only', $db_field);
|
||||
return t('Added a "read only" property to index entities.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear entity info cache, as entity controller classes hae changed.
|
||||
*/
|
||||
function search_api_update_7109() {
|
||||
cache_clear_all('entity_info:', 'cache', TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the "entity_type" field to "item_type" in the {search_api_index} table.
|
||||
*/
|
||||
function search_api_update_7110() {
|
||||
$table = 'search_api_index';
|
||||
// This index isn't used anymore.
|
||||
db_drop_index($table, 'entity_type');
|
||||
// Rename the "item_type" field (and change the description).
|
||||
$item_type = array(
|
||||
'description' => 'The type of items stored in this index.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
);
|
||||
// Also add the new "item_type" index, while we're at it.
|
||||
$keys_new['indexes']['item_type'] = array('item_type');
|
||||
db_change_field($table, 'entity_type', 'item_type', $item_type, $keys_new);
|
||||
// Mark all indexes in code as "OVERRIDDEN".
|
||||
db_update($table)
|
||||
->fields(array(
|
||||
'status' => 0x03,
|
||||
))
|
||||
->condition('status', 0x02)
|
||||
->execute();
|
||||
// Clear entity info caches.
|
||||
cache_clear_all('*', 'cache', TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the definition of the {search_api_item}.changed field.
|
||||
*/
|
||||
function search_api_update_7111() {
|
||||
$spec = array(
|
||||
'description' => 'Either a flag or a timestamp to indicate if or when the item was changed since it was last indexed.',
|
||||
'type' => 'int',
|
||||
'size' => 'big',
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
);
|
||||
db_change_field('search_api_item', 'changed', 'changed', $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the size of the {search_api_index}.options and {search_api_server}.options fields to "medium".
|
||||
*/
|
||||
function search_api_update_7112() {
|
||||
$spec = array(
|
||||
'description' => 'The options used to configure the service object.',
|
||||
'type' => 'text',
|
||||
'size' => 'medium',
|
||||
'serialize' => TRUE,
|
||||
'not null' => TRUE,
|
||||
);
|
||||
db_change_field('search_api_server', 'options', 'options', $spec);
|
||||
$spec = array(
|
||||
'description' => 'An array of additional arguments configuring this index.',
|
||||
'type' => 'text',
|
||||
'size' => 'medium',
|
||||
'serialize' => TRUE,
|
||||
'not null' => TRUE,
|
||||
);
|
||||
db_change_field('search_api_index', 'options', 'options', $spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes superfluous data from the stored index options.
|
||||
*/
|
||||
function search_api_update_7113() {
|
||||
$indexes = db_select('search_api_index', 'i')
|
||||
->fields('i')
|
||||
->execute();
|
||||
foreach ($indexes as $index) {
|
||||
$options = unserialize($index->options);
|
||||
// Weed out fields settings.
|
||||
if (!empty($options['fields'])) {
|
||||
foreach ($options['fields'] as $key => $field) {
|
||||
if (isset($field['indexed']) && !$field['indexed']) {
|
||||
unset($options['fields'][$key]);
|
||||
continue;
|
||||
}
|
||||
unset($options['fields'][$key]['name'], $options['fields'][$key]['indexed']);
|
||||
if (isset($field['boost']) && $field['boost'] == '1.0') {
|
||||
unset($options['fields'][$key]['boost']);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Weed out processor settings.
|
||||
if (!empty($options['processors'])) {
|
||||
// Only weed out settings for our own processors.
|
||||
$processors = array('search_api_case_ignore', 'search_api_html_filter', 'search_api_tokenizer', 'search_api_stopwords');
|
||||
foreach ($processors as $key) {
|
||||
if (empty($options['processors'][$key])) {
|
||||
continue;
|
||||
}
|
||||
$processor = $options['processors'][$key];
|
||||
if (empty($processor['settings']['fields'])) {
|
||||
continue;
|
||||
}
|
||||
$fields = array_filter($processor['settings']['fields']);
|
||||
if ($fields) {
|
||||
$fields = array_combine($fields, array_fill(0, count($fields), TRUE));
|
||||
}
|
||||
$options['processors'][$key]['settings']['fields'] = $fields;
|
||||
}
|
||||
}
|
||||
// Weed out settings for the „Aggregated fields“ data alteration.
|
||||
if (!empty($options['data_alter_callbacks']['search_api_alter_add_aggregation']['settings']['fields'])) {
|
||||
unset($options['data_alter_callbacks']['search_api_alter_add_aggregation']['settings']['actions']);
|
||||
$aggregated_fields = &$options['data_alter_callbacks']['search_api_alter_add_aggregation']['settings']['fields'];
|
||||
foreach ($aggregated_fields as $key => $field) {
|
||||
unset($aggregated_fields[$key]['actions']);
|
||||
if (!empty($field['fields'])) {
|
||||
$aggregated_fields[$key]['fields'] = array_values(array_filter($field['fields']));
|
||||
}
|
||||
}
|
||||
}
|
||||
$options = serialize($options);
|
||||
if ($options != $index->options) {
|
||||
// Mark the entity as overridden, in case it has been defined in code
|
||||
// only.
|
||||
$index->status |= 0x01;
|
||||
db_update('search_api_index')
|
||||
->fields(array(
|
||||
'options' => $options,
|
||||
'status' => $index->status,
|
||||
))
|
||||
->condition('id', $index->id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize watchdog messages.
|
||||
*/
|
||||
function search_api_update_7114() {
|
||||
if (db_table_exists('watchdog')) {
|
||||
try {
|
||||
$entries = db_select('watchdog', 'w')
|
||||
->fields('w', array('wid', 'message'))
|
||||
->condition('type', 'search_api')
|
||||
->execute();
|
||||
foreach ($entries as $entry) {
|
||||
db_update('watchdog')
|
||||
->fields(array(
|
||||
'message' => check_plain($entry->message),
|
||||
))
|
||||
->condition('wid', $entry->wid)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
throw new DrupalUpdateException(t('An exception occurred during the update: @msg.', array('@msg' => $e->getMessage())));
|
||||
}
|
||||
}
|
||||
}
|
2352
search_api.module
Normal file
2352
search_api.module
Normal file
File diff suppressed because it is too large
Load Diff
90
search_api.rules.inc
Normal file
90
search_api.rules.inc
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Search API Rules integration.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Implements hook_rules_action_info().
|
||||
*/
|
||||
function search_api_rules_action_info() {
|
||||
$items['search_api_index'] = array (
|
||||
'parameter' => array(
|
||||
'entity' => array(
|
||||
'type' => 'entity',
|
||||
'label' => t('Entity'),
|
||||
'description' => t('The item to index.'),
|
||||
),
|
||||
'index' => array(
|
||||
'type' => 'search_api_index',
|
||||
'label' => t('Index'),
|
||||
'description' => t('The index on which the item should be indexed. Leave blank to index on all indexes for this item type.'),
|
||||
'optional' => TRUE,
|
||||
'options list' => 'search_api_index_options_list',
|
||||
),
|
||||
'index_immediately' => array(
|
||||
'type' => 'boolean',
|
||||
'label' => t('Index immediately'),
|
||||
'description' => t('Activate for indexing the item right away, otherwise it will only be marked as dirty and indexed during the next cron run.'),
|
||||
'optional' => TRUE,
|
||||
'default value' => TRUE,
|
||||
'restriction' => 'input',
|
||||
),
|
||||
),
|
||||
'group' => t('Search API'),
|
||||
'access callback' => '_search_api_rules_access',
|
||||
'label' => t('Index an entity'),
|
||||
'base' => '_search_api_rules_action_index',
|
||||
);
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules access callback for search api actions.
|
||||
*/
|
||||
function _search_api_rules_access() {
|
||||
return user_access('administer search_api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules action for indexing an item.
|
||||
*/
|
||||
function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiIndex $index = NULL, $index_immediately = TRUE) {
|
||||
$type = $wrapper->type();
|
||||
$item_ids = array($wrapper->getIdentifier());
|
||||
|
||||
if (empty($index) && !$index_immediately) {
|
||||
search_api_track_item_change($type, $item_ids);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ($index) {
|
||||
$indexes = array($index);
|
||||
}
|
||||
else {
|
||||
$conditions = array(
|
||||
'enabled' => 1,
|
||||
'item_type' => $type,
|
||||
'read_only' => 0,
|
||||
);
|
||||
$indexes = search_api_index_load_multiple(FALSE, $conditions);
|
||||
if (!$indexes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($index_immediately) {
|
||||
foreach ($indexes as $index) {
|
||||
$indexed = search_api_index_specific_items_delayed($index, $item_ids);
|
||||
}
|
||||
}
|
||||
else {
|
||||
search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes);
|
||||
}
|
||||
}
|
||||
|
||||
function _search_api_rules_action_index_help() {
|
||||
return t('Queues an item for reindexing. If "index immediately" is disabled then the item will be indexed during the next cron run.');
|
||||
}
|
700
search_api.test
Normal file
700
search_api.test
Normal file
@ -0,0 +1,700 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class for testing Search API web functionality.
|
||||
*/
|
||||
class SearchApiWebTest extends DrupalWebTestCase {
|
||||
|
||||
protected $server_id;
|
||||
protected $index_id;
|
||||
|
||||
protected function assertText($text, $message = '', $group = 'Other') {
|
||||
return parent::assertText($text, $message ? $message : $text, $group);
|
||||
}
|
||||
|
||||
protected function drupalGet($path, array $options = array(), array $headers = array()) {
|
||||
$ret = parent::drupalGet($path, $options, $headers);
|
||||
$this->assertResponse(200, t('HTTP code 200 returned.'));
|
||||
return $ret;
|
||||
}
|
||||
|
||||
protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
|
||||
$ret = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post);
|
||||
$this->assertResponse(200, t('HTTP code 200 returned.'));
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Test search API framework',
|
||||
'description' => 'Tests basic functions of the Search API, like creating, editing and deleting servers and indexes.',
|
||||
'group' => 'Search API',
|
||||
);
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp('entity', 'search_api', 'search_api_test');
|
||||
}
|
||||
|
||||
public function testFramework() {
|
||||
$this->drupalLogin($this->drupalCreateUser(array('administer search_api')));
|
||||
// @todo Why is there no default index?
|
||||
//$this->deleteDefaultIndex();
|
||||
$this->insertItems();
|
||||
$this->checkOverview1();
|
||||
$this->createIndex();
|
||||
$this->insertItems(5);
|
||||
$this->createServer();
|
||||
$this->checkOverview2();
|
||||
$this->enableIndex();
|
||||
$this->searchNoResults();
|
||||
$this->indexItems();
|
||||
$this->searchSuccess();
|
||||
$this->editServer();
|
||||
$this->clearIndex();
|
||||
$this->searchNoResults();
|
||||
$this->deleteServer();
|
||||
}
|
||||
|
||||
protected function deleteDefaultIndex() {
|
||||
$this->drupalPost('admin/config/search/search_api/index/default_node_index/delete', array(), t('Confirm'));
|
||||
}
|
||||
|
||||
protected function insertItems($offset = 0) {
|
||||
$count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField();
|
||||
$this->insertItem(array(
|
||||
'id' => $offset + 1,
|
||||
'title' => 'Title 1',
|
||||
'body' => 'Body text 1.',
|
||||
'type' => 'Item',
|
||||
));
|
||||
$this->insertItem(array(
|
||||
'id' => $offset + 2,
|
||||
'title' => 'Title 2',
|
||||
'body' => 'Body text 2.',
|
||||
'type' => 'Item',
|
||||
));
|
||||
$this->insertItem(array(
|
||||
'id' => $offset + 3,
|
||||
'title' => 'Title 3',
|
||||
'body' => 'Body text 3.',
|
||||
'type' => 'Item',
|
||||
));
|
||||
$this->insertItem(array(
|
||||
'id' => $offset + 4,
|
||||
'title' => 'Title 4',
|
||||
'body' => 'Body text 4.',
|
||||
'type' => 'Page',
|
||||
));
|
||||
$this->insertItem(array(
|
||||
'id' => $offset + 5,
|
||||
'title' => 'Title 5',
|
||||
'body' => 'Body text 5.',
|
||||
'type' => 'Page',
|
||||
));
|
||||
$count = db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() - $count;
|
||||
$this->assertEqual($count, 5, t('@count items inserted.', array('@count' => $count)));
|
||||
}
|
||||
|
||||
protected function insertItem($values) {
|
||||
$this->drupalPost('search_api_test/insert', $values, t('Save'));
|
||||
}
|
||||
|
||||
protected function checkOverview1() {
|
||||
// This test fails for no apparent reason for drupal.org test bots.
|
||||
// Commenting them out for now.
|
||||
//$this->drupalGet('admin/config/search/search_api');
|
||||
//$this->assertText(t('There are no search servers or indexes defined yet.'), t('"No servers" message is displayed.'));
|
||||
}
|
||||
|
||||
protected function createIndex() {
|
||||
$values = array(
|
||||
'name' => '',
|
||||
'item_type' => '',
|
||||
'enabled' => 1,
|
||||
'description' => 'An index used for testing.',
|
||||
'server' => '',
|
||||
'options[cron_limit]' => 5,
|
||||
);
|
||||
$this->drupalPost('admin/config/search/search_api/add_index', $values, t('Create index'));
|
||||
$this->assertText(t('!name field is required.', array('!name' => t('Index name'))));
|
||||
$this->assertText(t('!name field is required.', array('!name' => t('Item type'))));
|
||||
|
||||
$this->index_id = $id = 'test_index';
|
||||
$values = array(
|
||||
'name' => 'Search API test index',
|
||||
'machine_name' => $id,
|
||||
'item_type' => 'search_api_test',
|
||||
'enabled' => 1,
|
||||
'description' => 'An index used for testing.',
|
||||
'server' => '',
|
||||
'options[cron_limit]' => 1,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Create index'));
|
||||
|
||||
$this->assertText(t('The index was successfully created. Please set up its indexed fields now.'), t('The index was successfully created.'));
|
||||
$found = strpos($this->getUrl(), 'admin/config/search/search_api/index/' . $id) !== FALSE;
|
||||
$this->assertTrue($found, t('Correct redirect.'));
|
||||
$index = search_api_index_load($id, TRUE);
|
||||
$this->assertEqual($index->name, $values['name'], t('Name correctly inserted.'));
|
||||
$this->assertEqual($index->item_type, $values['item_type'], t('Index item type correctly inserted.'));
|
||||
$this->assertFalse($index->enabled, t('Status correctly inserted.'));
|
||||
$this->assertEqual($index->description, $values['description'], t('Description correctly inserted.'));
|
||||
$this->assertNull($index->server, t('Index server correctly inserted.'));
|
||||
$this->assertEqual($index->options['cron_limit'], $values['options[cron_limit]'], t('Cron batch size correctly inserted.'));
|
||||
|
||||
$values = array(
|
||||
'additional[field]' => 'parent',
|
||||
);
|
||||
$this->drupalPost("admin/config/search/search_api/index/$id/fields", $values, t('Add fields'));
|
||||
$this->assertText(t('The available fields were successfully changed.'), t('Successfully added fields.'));
|
||||
$this->assertText('Parent » ID', t('!field displayed.', array('!field' => t('Added fields are'))));
|
||||
|
||||
$values = array(
|
||||
'fields[id][type]' => 'integer',
|
||||
'fields[id][boost]' => '1.0',
|
||||
'fields[id][indexed]' => 1,
|
||||
'fields[title][type]' => 'text',
|
||||
'fields[title][boost]' => '5.0',
|
||||
'fields[title][indexed]' => 1,
|
||||
'fields[body][type]' => 'text',
|
||||
'fields[body][boost]' => '1.0',
|
||||
'fields[body][indexed]' => 1,
|
||||
'fields[type][type]' => 'string',
|
||||
'fields[type][boost]' => '1.0',
|
||||
'fields[type][indexed]' => 1,
|
||||
'fields[parent:id][type]' => 'integer',
|
||||
'fields[parent:id][boost]' => '1.0',
|
||||
'fields[parent:id][indexed]' => 1,
|
||||
'fields[parent:title][type]' => 'text',
|
||||
'fields[parent:title][boost]' => '5.0',
|
||||
'fields[parent:title][indexed]' => 1,
|
||||
'fields[parent:body][type]' => 'text',
|
||||
'fields[parent:body][boost]' => '1.0',
|
||||
'fields[parent:body][indexed]' => 1,
|
||||
'fields[parent:type][type]' => 'string',
|
||||
'fields[parent:type][boost]' => '1.0',
|
||||
'fields[parent:type][indexed]' => 1,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Save changes'));
|
||||
$this->assertText(t('The indexed fields were successfully changed. The index was cleared and will have to be re-indexed with the new settings.'), t('Field settings saved.'));
|
||||
|
||||
$values = array(
|
||||
'callbacks[search_api_alter_add_url][status]' => 1,
|
||||
'callbacks[search_api_alter_add_url][weight]' => 0,
|
||||
'callbacks[search_api_alter_add_aggregation][status]' => 1,
|
||||
'callbacks[search_api_alter_add_aggregation][weight]' => 10,
|
||||
'processors[search_api_case_ignore][status]' => 1,
|
||||
'processors[search_api_case_ignore][weight]' => 0,
|
||||
'processors[search_api_case_ignore][settings][fields][title]' => 1,
|
||||
'processors[search_api_case_ignore][settings][fields][body]' => 1,
|
||||
'processors[search_api_case_ignore][settings][fields][parent:title]' => 1,
|
||||
'processors[search_api_case_ignore][settings][fields][parent:body]' => 1,
|
||||
'processors[search_api_tokenizer][status]' => 1,
|
||||
'processors[search_api_tokenizer][weight]' => 20,
|
||||
'processors[search_api_tokenizer][settings][spaces]' => '[^\p{L}\p{N}]',
|
||||
'processors[search_api_tokenizer][settings][ignorable]' => '[-]',
|
||||
'processors[search_api_tokenizer][settings][fields][title]' => 1,
|
||||
'processors[search_api_tokenizer][settings][fields][body]' => 1,
|
||||
'processors[search_api_tokenizer][settings][fields][parent:title]' => 1,
|
||||
'processors[search_api_tokenizer][settings][fields][parent:body]' => 1,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Add new field'));
|
||||
$values = array(
|
||||
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][name]' => 'Test fulltext field',
|
||||
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][type]' => 'fulltext',
|
||||
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][title]' => 1,
|
||||
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][body]' => 1,
|
||||
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][parent:title]' => 1,
|
||||
'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][parent:body]' => 1,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Save configuration'));
|
||||
$this->assertText(t("The search index' workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), t('Workflow successfully edited.'));
|
||||
|
||||
$this->drupalGet("admin/config/search/search_api/index/$id");
|
||||
$this->assertTitle('Search API test index | Drupal', t('Correct title when viewing index.'));
|
||||
$this->assertText('An index used for testing.', t('!field displayed.', array('!field' => t('Description'))));
|
||||
$this->assertText('Search API test entity', t('!field displayed.', array('!field' => t('Item type'))));
|
||||
$this->assertText(format_plural(1, '1 item per cron batch.', '@count items per cron batch.'), t('!field displayed.', array('!field' => t('Cron batch size'))));
|
||||
|
||||
$this->drupalGet("admin/config/search/search_api/index/$id/status");
|
||||
$this->assertText(t('The index is currently disabled.'), t('"Disabled" status displayed.'));
|
||||
}
|
||||
|
||||
protected function createServer() {
|
||||
$values = array(
|
||||
'name' => '',
|
||||
'enabled' => 1,
|
||||
'description' => 'A server used for testing.',
|
||||
'class' => '',
|
||||
);
|
||||
$this->drupalPost('admin/config/search/search_api/add_server', $values, t('Create server'));
|
||||
$this->assertText(t('!name field is required.', array('!name' => t('Server name'))));
|
||||
$this->assertText(t('!name field is required.', array('!name' => t('Service class'))));
|
||||
|
||||
$this->server_id = $id = 'test_server';
|
||||
$values = array(
|
||||
'name' => 'Search API test server',
|
||||
'machine_name' => $id,
|
||||
'enabled' => 1,
|
||||
'description' => 'A server used for testing.',
|
||||
'class' => 'search_api_test_service',
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Create server'));
|
||||
|
||||
$values2 = array(
|
||||
'options[form][test]' => 'search_api_test foo bar',
|
||||
);
|
||||
$this->drupalPost(NULL, $values2, t('Create server'));
|
||||
|
||||
$this->assertText(t('The server was successfully created.'));
|
||||
$found = strpos($this->getUrl(), 'admin/config/search/search_api/server/' . $id) !== FALSE;
|
||||
$this->assertTrue($found, t('Correct redirect.'));
|
||||
$server = search_api_server_load($id, TRUE);
|
||||
$this->assertEqual($server->name, $values['name'], t('Name correctly inserted.'));
|
||||
$this->assertTrue($server->enabled, t('Status correctly inserted.'));
|
||||
$this->assertEqual($server->description, $values['description'], t('Description correctly inserted.'));
|
||||
$this->assertEqual($server->class, $values['class'], t('Service class correctly inserted.'));
|
||||
$this->assertEqual($server->options['test'], $values2['options[form][test]'], t('Service options correctly inserted.'));
|
||||
$this->assertTitle('Search API test server | Drupal', t('Correct title when viewing server.'));
|
||||
$this->assertText('A server used for testing.', t('!field displayed.', array('!field' => t('Description'))));
|
||||
$this->assertText('search_api_test_service', t('!field displayed.', array('!field' => t('Service name'))));
|
||||
$this->assertText('search_api_test_service description', t('!field displayed.', array('!field' => t('Service description'))));
|
||||
$this->assertText('search_api_test foo bar', t('!field displayed.', array('!field' => t('Service options'))));
|
||||
}
|
||||
|
||||
protected function checkOverview2() {
|
||||
$this->drupalGet('admin/config/search/search_api');
|
||||
$this->assertText('Search API test server', t('!field displayed.', array('!field' => t('Server'))));
|
||||
$this->assertText('Search API test index', t('!field displayed.', array('!field' => t('Index'))));
|
||||
$this->assertNoText(t('There are no search servers or indexes defined yet.'), t('"No servers" message not displayed.'));
|
||||
}
|
||||
|
||||
protected function enableIndex() {
|
||||
$values = array(
|
||||
'server' => $this->server_id,
|
||||
);
|
||||
$this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/edit", $values, t('Save settings'));
|
||||
$this->assertText(t('The search index was successfully edited.'));
|
||||
$this->assertText('Search API test server', t('!field displayed.', array('!field' => t('Server'))));
|
||||
|
||||
$this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/enable");
|
||||
$this->assertText(t('The index was successfully enabled.'));
|
||||
}
|
||||
|
||||
protected function searchNoResults() {
|
||||
$this->drupalGet('search_api_test/query/' . $this->index_id);
|
||||
$this->assertText('result count = 0', t('No search results returned without indexing.'));
|
||||
$this->assertText('results = ()', t('No search results returned without indexing.'));
|
||||
}
|
||||
|
||||
protected function indexItems() {
|
||||
$this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status");
|
||||
$this->assertText(t('The index is currently enabled.'), t('"Enabled" status displayed.'));
|
||||
$this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), t('!field displayed.', array('!field' => t('Correct index status'))));
|
||||
$this->assertText(t('Index now'), t('"Index now" button found.'));
|
||||
$this->assertText(t('Clear index'), t('"Clear index" button found.'));
|
||||
$this->assertNoText(t('Re-index content'), t('"Re-index" button not found.'));
|
||||
|
||||
// Here we test the indexing + the warning message when some items
|
||||
// can not be indexed.
|
||||
// The server refuses (for test purpose) to index items with IDs that are
|
||||
// multiples of 8 unless the "search_api_test_index_all" variable is set.
|
||||
$values = array(
|
||||
'limit' => 8,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Index now'));
|
||||
$this->assertText(t('Successfully indexed @count items.', array('@count' => 7)));
|
||||
$this->assertText(t('1 item could not be indexed. Check the logs for details.'), t('Index errors warning is displayed.'));
|
||||
$this->assertNoText(t("Couldn't index items. Check the logs for details."), t("Index error isn't displayed."));
|
||||
$this->assertText(t('About @percentage% of all items have been indexed in their latest version (@indexed / @total).', array('@indexed' => 7, '@total' => 10, '@percentage' => 70)), t('!field displayed.', array('!field' => t('Correct index status'))));
|
||||
$this->assertText(t('Re-indexing'), t('"Re-index" button found.'));
|
||||
|
||||
// Here we're testing the error message when no item could be indexed.
|
||||
// The item with ID 8 is still not indexed.
|
||||
$values = array(
|
||||
'limit' => 1,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Index now'));
|
||||
$this->assertNoPattern('/' . str_replace('144', '-?\d*', t('Successfully indexed @count items.', array('@count' => 144))) . '/', t('No items could be indexed.'));
|
||||
$this->assertNoText(t('1 item could not be indexed. Check the logs for details.'), t("Index errors warning isn't displayed."));
|
||||
$this->assertText(t("Couldn't index items. Check the logs for details."), t('Index error is displayed.'));
|
||||
|
||||
// Here we test the indexing of all the remaining items.
|
||||
variable_set('search_api_test_index_all', TRUE);
|
||||
$values = array(
|
||||
'limit' => -1,
|
||||
);
|
||||
$this->drupalPost(NULL, $values, t('Index now'));
|
||||
$this->assertText(t('Successfully indexed @count items.', array('@count' => 3)));
|
||||
$this->assertNoText(t("Some items couldn't be indexed. Check the logs for details."), t("Index errors warning isn't displayed."));
|
||||
$this->assertNoText(t("Couldn't index items. Check the logs for details."), t("Index error isn't displayed."));
|
||||
$this->assertText(t('All items have been indexed (@indexed / @total).', array('@indexed' => 10, '@total' => 10)), t('!field displayed.', array('!field' => t('Correct index status'))));
|
||||
$this->assertNoText(t('Index now'), t('"Index now" button no longer displayed.'));
|
||||
}
|
||||
|
||||
protected function searchSuccess() {
|
||||
$this->drupalGet('search_api_test/query/' . $this->index_id);
|
||||
$this->assertText('result count = 10', t('Correct search result count returned after indexing.'));
|
||||
$this->assertText('results = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)', t('Correct search results returned after indexing.'));
|
||||
|
||||
$this->drupalGet('search_api_test/query/' . $this->index_id . '/foo/2/4');
|
||||
$this->assertText('result count = 10', t('Correct search result count with ranged query.'));
|
||||
$this->assertText('results = (3, 4, 5, 6)', t('Correct search results with ranged query.'));
|
||||
}
|
||||
|
||||
protected function editServer() {
|
||||
$values = array(
|
||||
'name' => 'test-name-foo',
|
||||
'description' => 'test-description-bar',
|
||||
'options[form][test]' => 'test-test-baz',
|
||||
);
|
||||
$this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/edit", $values, t('Save settings'));
|
||||
$this->assertText(t('The search server was successfully edited.'));
|
||||
$this->assertText('test-name-foo', t('!field changed.', array('!field' => t('Name'))));
|
||||
$this->assertText('test-description-bar', t('!field changed.', array('!field' => t('Description'))));
|
||||
$this->assertText('test-test-baz', t('!field changed.', array('!field' => t('Service options'))));
|
||||
}
|
||||
|
||||
protected function clearIndex() {
|
||||
$this->drupalPost("admin/config/search/search_api/index/{$this->index_id}/status", array(), t('Clear index'));
|
||||
$this->assertText(t('The index was successfully cleared.'));
|
||||
$this->assertText(t('All items still need to be indexed (@total total).', array('@total' => 10)), t('!field displayed.', array('!field' => t('Correct index status'))));
|
||||
}
|
||||
|
||||
protected function deleteServer() {
|
||||
$this->drupalPost("admin/config/search/search_api/server/{$this->server_id}/delete", array(), t('Confirm'));
|
||||
$this->assertNoText('test-name-foo', t('Server no longer listed.'));
|
||||
$this->drupalGet("admin/config/search/search_api/index/{$this->index_id}/status");
|
||||
$this->assertText(t('The index is currently disabled.'), t('The index was disabled and removed from the server.'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Class with unit tests testing small fragments of the Search API.
|
||||
*
|
||||
* Due to severe limitations for "real" unit tests, this still has to be a
|
||||
* subclass of DrupalWebTestCase.
|
||||
*/
|
||||
class SearchApiUnitTest extends DrupalWebTestCase {
|
||||
|
||||
protected $index;
|
||||
|
||||
protected function assertEqual($first, $second, $message = '', $group = 'Other') {
|
||||
if (is_array($first) && is_array($second)) {
|
||||
return $this->assertTrue($this->deepEquals($first, $second), $message, $group);
|
||||
}
|
||||
else {
|
||||
return parent::assertEqual($first, $second, $message, $group);
|
||||
}
|
||||
}
|
||||
|
||||
protected function deepEquals($first, $second) {
|
||||
if (!is_array($first) || !is_array($second)) {
|
||||
return $first == $second;
|
||||
}
|
||||
$first = array_merge($first);
|
||||
$second = array_merge($second);
|
||||
foreach ($first as $key => $value) {
|
||||
if (!array_key_exists($key, $second) || !$this->deepEquals($value, $second[$key])) {
|
||||
return FALSE;
|
||||
}
|
||||
unset($second[$key]);
|
||||
}
|
||||
return empty($second);
|
||||
}
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Test search API components',
|
||||
'description' => 'Tests some independent components of the Search API, like the processors.',
|
||||
'group' => 'Search API',
|
||||
);
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp('entity', 'search_api');
|
||||
$this->index = entity_create('search_api_index', array(
|
||||
'id' => 1,
|
||||
'name' => 'test',
|
||||
'enabled' => 1,
|
||||
'item_type' => 'user',
|
||||
'options' => array(
|
||||
'fields' => array(
|
||||
'name' => array(
|
||||
'type' => 'text',
|
||||
),
|
||||
'mail' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'search_api_language' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function testUnits() {
|
||||
$this->checkQueryParseKeys();
|
||||
$this->checkIgnoreCaseProcessor();
|
||||
$this->checkTokenizer();
|
||||
$this->checkHtmlFilter();
|
||||
}
|
||||
|
||||
public function checkQueryParseKeys() {
|
||||
$options['parse mode'] = 'direct';
|
||||
$mode = &$options['parse mode'];
|
||||
$num = 1;
|
||||
$query = new SearchApiQuery($this->index, $options);
|
||||
$modes = $query->parseModes();
|
||||
|
||||
$query->keys('foo');
|
||||
$this->assertEqual($query->getKeys(), 'foo', t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
$query->keys('foo bar');
|
||||
$this->assertEqual($query->getKeys(), 'foo bar', t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
$query->keys('(foo bar) OR "bar baz"');
|
||||
$this->assertEqual($query->getKeys(), '(foo bar) OR "bar baz"', t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
|
||||
$mode = 'single';
|
||||
$num = 1;
|
||||
$query = new SearchApiQuery($this->index, $options);
|
||||
|
||||
$query->keys('foo');
|
||||
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
$query->keys('foo bar');
|
||||
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo bar'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
$query->keys('(foo bar) OR "bar baz"');
|
||||
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', '(foo bar) OR "bar baz"'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
|
||||
$mode = 'terms';
|
||||
$num = 1;
|
||||
$query = new SearchApiQuery($this->index, $options);
|
||||
|
||||
$query->keys('foo');
|
||||
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
$query->keys('foo bar');
|
||||
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'foo', 'bar'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
$query->keys('(foo bar) OR "bar baz"');
|
||||
$this->assertEqual($query->getKeys(), array('(foo', 'bar)', 'OR', 'bar baz', '#conjunction' => 'AND'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
// http://drupal.org/node/1468678
|
||||
$query->keys('"Münster"');
|
||||
$this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'Münster'), t('"@mode" parse mode, test !num.', array('@mode' => $modes[$mode]['name'], '!num' => $num++)));
|
||||
}
|
||||
|
||||
public function checkIgnoreCaseProcessor() {
|
||||
$types = search_api_field_types();
|
||||
$orig = 'Foo bar BaZ, ÄÖÜÀÁ<>»«.';
|
||||
$processed = drupal_strtolower($orig);
|
||||
$items = array(
|
||||
1 => array(
|
||||
'name' => array(
|
||||
'type' => 'text',
|
||||
'original_type' => 'text',
|
||||
'value' => $orig,
|
||||
),
|
||||
'mail' => array(
|
||||
'type' => 'string',
|
||||
'original_type' => 'text',
|
||||
'value' => $orig,
|
||||
),
|
||||
'search_api_language' => array(
|
||||
'type' => 'string',
|
||||
'original_type' => 'string',
|
||||
'value' => LANGUAGE_NONE,
|
||||
),
|
||||
),
|
||||
);
|
||||
$keys1 = $keys2 = array(
|
||||
'foo',
|
||||
'bar baz',
|
||||
'foobar1',
|
||||
'#conjunction' => 'AND',
|
||||
);
|
||||
$filters1 = array(
|
||||
array('name', 'foo', '='),
|
||||
array('mail', 'BAR', '='),
|
||||
);
|
||||
$filters2 = array(
|
||||
array('name', 'foo', '='),
|
||||
array('mail', 'bar', '='),
|
||||
);
|
||||
|
||||
$processor = new SearchApiIgnoreCase($this->index, array('fields' => array('name' => 'name')));
|
||||
$tmp = $items;
|
||||
$processor->preprocessIndexItems($tmp);
|
||||
$this->assertEqual($tmp[1]['name']['value'], $processed, t('!type field was processed.', array('!type' => 'name')));
|
||||
$this->assertEqual($tmp[1]['mail']['value'], $orig, t("!type field wasn't processed.", array('!type' => 'mail')));
|
||||
|
||||
$query = new SearchApiQuery($this->index);
|
||||
$query->keys('Foo "baR BaZ" fOObAr1');
|
||||
$query->condition('name', 'FOO');
|
||||
$query->condition('mail', 'BAR');
|
||||
$processor->preprocessSearchQuery($query);
|
||||
$this->assertEqual($query->getKeys(), $keys1, t('Search keys were processed correctly.'));
|
||||
$this->assertEqual($query->getFilter()->getFilters(), $filters1, t('Filters were processed correctly.'));
|
||||
|
||||
$processor = new SearchApiIgnoreCase($this->index, array('fields' => array('name' => 'name', 'mail' => 'mail')));
|
||||
$tmp = $items;
|
||||
$processor->preprocessIndexItems($tmp);
|
||||
$this->assertEqual($tmp[1]['name']['value'], $processed, t('!type field was processed.', array('!type' => 'name')));
|
||||
$this->assertEqual($tmp[1]['mail']['value'], $processed, t('!type field was processed.', array('!type' => 'mail')));
|
||||
|
||||
$query = new SearchApiQuery($this->index);
|
||||
$query->keys('Foo "baR BaZ" fOObAr1');
|
||||
$query->condition('name', 'FOO');
|
||||
$query->condition('mail', 'BAR');
|
||||
$processor->preprocessSearchQuery($query);
|
||||
$this->assertEqual($query->getKeys(), $keys2, t('Search keys were processed correctly.'));
|
||||
$this->assertEqual($query->getFilter()->getFilters(), $filters2, t('Filters were processed correctly.'));
|
||||
}
|
||||
|
||||
public function checkTokenizer() {
|
||||
$orig = 'Foo bar1 BaZ, La-la-la.';
|
||||
$processed1 = array(
|
||||
array(
|
||||
'value' => 'Foo',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'bar1',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'BaZ',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'Lalala',
|
||||
'score' => 1,
|
||||
),
|
||||
);
|
||||
$processed2 = array(
|
||||
array(
|
||||
'value' => 'Foob',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'r1B',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'Z,L',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'l',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => 'l',
|
||||
'score' => 1,
|
||||
),
|
||||
array(
|
||||
'value' => '.',
|
||||
'score' => 1,
|
||||
),
|
||||
);
|
||||
$items = array(
|
||||
1 => array(
|
||||
'name' => array(
|
||||
'type' => 'text',
|
||||
'original_type' => 'text',
|
||||
'value' => $orig,
|
||||
),
|
||||
'search_api_language' => array(
|
||||
'type' => 'string',
|
||||
'original_type' => 'string',
|
||||
'value' => LANGUAGE_NONE,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$processor = new SearchApiTokenizer($this->index, array('fields' => array('name' => 'name'), 'spaces' => '[^\p{L}\p{N}]', 'ignorable' => '[-]'));
|
||||
$tmp = $items;
|
||||
$processor->preprocessIndexItems($tmp);
|
||||
$this->assertEqual($tmp[1]['name']['value'], $processed1, t('Value was correctly tokenized with default settings.'));
|
||||
|
||||
$query = new SearchApiQuery($this->index, array('parse mode' => 'direct'));
|
||||
$query->keys("foo \"bar-baz\" \n\t foobar1");
|
||||
$processor->preprocessSearchQuery($query);
|
||||
$this->assertEqual($query->getKeys(), 'foo barbaz foobar1', t('Search keys were processed correctly.'));
|
||||
|
||||
$processor = new SearchApiTokenizer($this->index, array('fields' => array('name' => 'name'), 'spaces' => '[-a]', 'ignorable' => '\s'));
|
||||
$tmp = $items;
|
||||
$processor->preprocessIndexItems($tmp);
|
||||
$this->assertEqual($tmp[1]['name']['value'], $processed2, t('Value was correctly tokenized with custom settings.'));
|
||||
|
||||
$query = new SearchApiQuery($this->index, array('parse mode' => 'direct'));
|
||||
$query->keys("foo \"bar-baz\" \n\t foobar1");
|
||||
$processor->preprocessSearchQuery($query);
|
||||
$this->assertEqual($query->getKeys(), 'foo"b r b z"foob r1', t('Search keys were processed correctly.'));
|
||||
}
|
||||
|
||||
public function checkHtmlFilter() {
|
||||
$orig = <<<END
|
||||
This is <em lang="en" title =
|
||||
"something">a test</em>.
|
||||
How to write <strong>links to <em>other sites</em></strong>: <a href="URL" title="MOUSEOVER TEXT">TEXT</a>.
|
||||
< signs can be <A HREF="http://example.com/topic/html-escapes" TITLE = 'HTML "escapes"'
|
||||
TARGET = '_blank'>escaped</A> with "&lt;".
|
||||
<img src = "foo.png" alt = "someone's image" />
|
||||
END;
|
||||
$tags = <<<END
|
||||
em = 1.5
|
||||
strong = 2
|
||||
END;
|
||||
$processed1 = array(
|
||||
array('value' => 'This', 'score' => 1),
|
||||
array('value' => 'is', 'score' => 1),
|
||||
array('value' => 'something', 'score' => 1.5),
|
||||
array('value' => 'a', 'score' => 1.5),
|
||||
array('value' => 'test', 'score' => 1.5),
|
||||
array('value' => 'How', 'score' => 1),
|
||||
array('value' => 'to', 'score' => 1),
|
||||
array('value' => 'write', 'score' => 1),
|
||||
array('value' => 'links', 'score' => 2),
|
||||
array('value' => 'to', 'score' => 2),
|
||||
array('value' => 'other', 'score' => 3),
|
||||
array('value' => 'sites', 'score' => 3),
|
||||
array('value' => '<a', 'score' => 1),
|
||||
array('value' => 'href="URL"', 'score' => 1),
|
||||
array('value' => 'title="MOUSEOVER', 'score' => 1),
|
||||
array('value' => 'TEXT">TEXT</a>', 'score' => 1),
|
||||
array('value' => '<', 'score' => 1),
|
||||
array('value' => 'signs', 'score' => 1),
|
||||
array('value' => 'can', 'score' => 1),
|
||||
array('value' => 'be', 'score' => 1),
|
||||
array('value' => 'HTML', 'score' => 1),
|
||||
array('value' => '"escapes"', 'score' => 1),
|
||||
array('value' => 'escaped', 'score' => 1),
|
||||
array('value' => 'with', 'score' => 1),
|
||||
array('value' => '"<"', 'score' => 1),
|
||||
array('value' => 'someone\'s', 'score' => 1),
|
||||
array('value' => 'image', 'score' => 1),
|
||||
);
|
||||
$items = array(
|
||||
1 => array(
|
||||
'name' => array(
|
||||
'type' => 'text',
|
||||
'original_type' => 'text',
|
||||
'value' => $orig,
|
||||
),
|
||||
'search_api_language' => array(
|
||||
'type' => 'string',
|
||||
'original_type' => 'string',
|
||||
'value' => LANGUAGE_NONE,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$tmp = $items;
|
||||
$processor = new SearchApiHtmlFilter($this->index, array('fields' => array('name' => 'name'), 'title' => TRUE, 'alt' => TRUE, 'tags' => $tags));
|
||||
$processor->preprocessIndexItems($tmp);
|
||||
$processor = new SearchApiTokenizer($this->index, array('fields' => array('name' => 'name'), 'spaces' => '[\s.:]', 'ignorable' => ''));
|
||||
$processor->preprocessIndexItems($tmp);
|
||||
$this->assertEqual($tmp[1]['name']['value'], $processed1, t('Text was correctly processed.'));
|
||||
}
|
||||
|
||||
}
|
18
tests/search_api_test.info
Normal file
18
tests/search_api_test.info
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
name = Search API test
|
||||
description = "Some dummy implementations for testing the Search API."
|
||||
core = 7.x
|
||||
package = Search
|
||||
|
||||
dependencies[] = search_api
|
||||
|
||||
files[] = search_api_test.module
|
||||
|
||||
hidden = TRUE
|
||||
|
||||
; Information added by drupal.org packaging script on 2013-01-09
|
||||
version = "7.x-1.4"
|
||||
core = "7.x"
|
||||
project = "search_api"
|
||||
datestamp = "1357726719"
|
||||
|
43
tests/search_api_test.install
Normal file
43
tests/search_api_test.install
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the Search API test module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function search_api_test_schema() {
|
||||
$schema['search_api_test'] = array(
|
||||
'description' => 'Stores instances of a test entity.',
|
||||
'fields' => array(
|
||||
'id' => array(
|
||||
'description' => 'The primary identifier for an item.',
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'title' => array(
|
||||
'description' => 'The title of the item.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'body' => array(
|
||||
'description' => 'A text belonging to the item.',
|
||||
'type' => 'text',
|
||||
'not null' => FALSE,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => 'A string identifying the type of item.',
|
||||
'type' => 'varchar',
|
||||
'length' => 50,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
),
|
||||
'primary key' => array('id'),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
316
tests/search_api_test.module
Normal file
316
tests/search_api_test.module
Normal file
@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function search_api_test_menu() {
|
||||
return array(
|
||||
'search_api_test/insert' => array(
|
||||
'title' => 'Insert item',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('search_api_test_insert_item'),
|
||||
'access callback' => TRUE,
|
||||
),
|
||||
'search_api_test/%search_api_test' => array(
|
||||
'title' => 'View item',
|
||||
'page callback' => 'search_api_test_view',
|
||||
'page arguments' => array(1),
|
||||
'access callback' => TRUE,
|
||||
),
|
||||
'search_api_test/query/%search_api_index' => array(
|
||||
'title' => 'Search query',
|
||||
'page callback' => 'search_api_test_query',
|
||||
'page arguments' => array(2),
|
||||
'access callback' => TRUE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form callback for inserting an item.
|
||||
*/
|
||||
function search_api_test_insert_item(array $form, array &$form_state) {
|
||||
return array(
|
||||
'id' => array(
|
||||
'#type' => 'textfield',
|
||||
),
|
||||
'title' => array(
|
||||
'#type' => 'textfield',
|
||||
),
|
||||
'body' => array(
|
||||
'#type' => 'textarea',
|
||||
),
|
||||
'type' => array(
|
||||
'#type' => 'textfield',
|
||||
),
|
||||
'submit' => array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit callback for search_api_test_insert_item().
|
||||
*/
|
||||
function search_api_test_insert_item_submit(array $form, array &$form_state) {
|
||||
form_state_values_clean($form_state);
|
||||
db_insert('search_api_test')->fields($form_state['values'])->execute();
|
||||
module_invoke_all('entity_insert', search_api_test_load($form_state['values']['id']), 'search_api_test');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load handler for search_api_test entities.
|
||||
*/
|
||||
function search_api_test_load($id) {
|
||||
$ret = entity_load('search_api_test', array($id));
|
||||
return $ret ? array_shift($ret) : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback for displaying search_api_test entities.
|
||||
*/
|
||||
function search_api_test_view($entity) {
|
||||
return array('text' => nl2br(check_plain(print_r($entity, TRUE))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback for executing a search.
|
||||
*/
|
||||
function search_api_test_query(SearchApiIndex $index, $keys = 'foo bar', $offset = 0, $limit = 10, $fields = NULL, $sort = NULL, $filters = NULL) {
|
||||
// Slight "hack" for testing complex queries.
|
||||
if ($keys == '|COMPLEX|') {
|
||||
$keys = array(
|
||||
'#conjunction' => 'AND',
|
||||
'test',
|
||||
array(
|
||||
'#conjunction' => 'OR',
|
||||
'baz',
|
||||
'foobar',
|
||||
),
|
||||
array(
|
||||
'#conjunction' => 'AND',
|
||||
'#negation' => TRUE,
|
||||
'bar',
|
||||
),
|
||||
);
|
||||
}
|
||||
$query = $index->query()
|
||||
->keys($keys)
|
||||
->range($offset, $limit);
|
||||
if ($fields) {
|
||||
$query->fields(explode(',', $fields));
|
||||
}
|
||||
if ($sort) {
|
||||
$sort = explode(',', $sort);
|
||||
$query->sort($sort[0], $sort[1]);
|
||||
}
|
||||
else {
|
||||
$query->sort('search_api_id', 'ASC');
|
||||
}
|
||||
if ($filters) {
|
||||
$filters = explode(',', $filters);
|
||||
foreach ($filters as $filter) {
|
||||
$filter = explode('=', $filter);
|
||||
$query->condition($filter[0], $filter[1]);
|
||||
}
|
||||
}
|
||||
$result = $query->execute();
|
||||
|
||||
$ret = '';
|
||||
$ret .= 'result count = ' . (int) $result['result count'] . '<br/>';
|
||||
$ret .= 'results = (' . (empty($result['results']) ? '' : implode(', ', array_keys($result['results']))) . ')<br/>';
|
||||
$ret .= 'warnings = (' . (empty($result['warnings']) ? '' : '"' . implode('", "', $result['warnings']) . '"') . ')<br/>';
|
||||
$ret .= 'ignored = (' . (empty($result['ignored']) ? '' : implode(', ', $result['ignored'])) . ')<br/>';
|
||||
$ret .= nl2br(check_plain(print_r($result['performance'], TRUE)));
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_info().
|
||||
*/
|
||||
function search_api_test_entity_info() {
|
||||
return array(
|
||||
'search_api_test' => array(
|
||||
'label' => 'Search API test entity',
|
||||
'base table' => 'search_api_test',
|
||||
'uri callback' => 'search_api_test_uri',
|
||||
'entity keys' => array(
|
||||
'id' => 'id',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_property_info().
|
||||
*/
|
||||
function search_api_test_entity_property_info() {
|
||||
$info['search_api_test']['properties'] = array(
|
||||
'id' => array(
|
||||
'label' => 'ID',
|
||||
'type' => 'integer',
|
||||
'description' => 'The primary identifier for a server.',
|
||||
),
|
||||
'title' => array(
|
||||
'label' => 'Title',
|
||||
'type' => 'text',
|
||||
'description' => 'The title of the item.',
|
||||
'required' => TRUE,
|
||||
),
|
||||
'body' => array(
|
||||
'label' => 'Body',
|
||||
'type' => 'text',
|
||||
'description' => 'A text belonging to the item.',
|
||||
'sanitize' => 'filter_xss',
|
||||
'required' => TRUE,
|
||||
),
|
||||
'type' => array(
|
||||
'label' => 'Type',
|
||||
'type' => 'text',
|
||||
'description' => 'A string identifying the type of item.',
|
||||
'required' => TRUE,
|
||||
),
|
||||
'parent' => array(
|
||||
'label' => 'Parent',
|
||||
'type' => 'search_api_test',
|
||||
'description' => "The item's parent.",
|
||||
'getter callback' => 'search_api_test_parent',
|
||||
),
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* URI callback for test entity.
|
||||
*/
|
||||
function search_api_test_uri($entity) {
|
||||
return array(
|
||||
'path' => 'search_api_test/' . $entity->id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent callback.
|
||||
*/
|
||||
function search_api_test_parent($entity) {
|
||||
return search_api_test_load($entity->id - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_search_api_service_info().
|
||||
*/
|
||||
function search_api_test_search_api_service_info() {
|
||||
$services['search_api_test_service'] = array(
|
||||
'name' => 'search_api_test_service',
|
||||
'description' => 'search_api_test_service description',
|
||||
'class' => 'SearchApiTestService',
|
||||
);
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test service class.
|
||||
*/
|
||||
class SearchApiTestService extends SearchApiAbstractService {
|
||||
|
||||
public function configurationForm(array $form, array &$form_state) {
|
||||
$form = array(
|
||||
'test' => array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => 'Test option',
|
||||
),
|
||||
);
|
||||
|
||||
if (!empty($this->options)) {
|
||||
$form['test']['#default_value'] = $this->options['test'];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function indexItems(SearchApiIndex $index, array $items) {
|
||||
// Refuse to index items with IDs that are multiples of 8 unless the
|
||||
// "search_api_test_index_all" variable is set.
|
||||
if (variable_get('search_api_test_index_all', FALSE)) {
|
||||
return $this->index($index, array_keys($items));
|
||||
}
|
||||
$ret = array();
|
||||
foreach ($items as $id => $item) {
|
||||
if ($id % 8) {
|
||||
$ret[] = $id;
|
||||
}
|
||||
}
|
||||
return $this->index($index, $ret);
|
||||
}
|
||||
|
||||
protected function index(SearchApiIndex $index, array $ids) {
|
||||
$this->options += array('indexes' => array());
|
||||
$this->options['indexes'] += array($index->machine_name => array());
|
||||
$this->options['indexes'][$index->machine_name] += drupal_map_assoc($ids);
|
||||
sort($this->options['indexes'][$index->machine_name]);
|
||||
$this->server->save();
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override so deleteItems() isn't called which would otherwise lead to the
|
||||
* server being updated and, eventually, to a notice because there is no
|
||||
* server to be updated anymore.
|
||||
*/
|
||||
public function preDelete() {}
|
||||
|
||||
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
|
||||
if ($ids == 'all') {
|
||||
if ($index) {
|
||||
$this->options['indexes'][$index->machine_name] = array();
|
||||
}
|
||||
else {
|
||||
$this->options['indexes'] = array();
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($ids as $id) {
|
||||
unset($this->options['indexes'][$index->machine_name][$id]);
|
||||
}
|
||||
}
|
||||
$this->server->save();
|
||||
}
|
||||
|
||||
public function search(SearchApiQueryInterface $query) {
|
||||
$options = $query->getOptions();
|
||||
$ret = array();
|
||||
$index_id = $query->getIndex()->machine_name;
|
||||
if (empty($this->options['indexes'][$index_id])) {
|
||||
return array(
|
||||
'result count' => 0,
|
||||
'results' => array(),
|
||||
);
|
||||
}
|
||||
$items = $this->options['indexes'][$index_id];
|
||||
$min = isset($options['offset']) ? $options['offset'] : 0;
|
||||
$max = $min + (isset($options['limit']) ? $options['limit'] : count($items));
|
||||
$i = 0;
|
||||
$ret['result count'] = count($items);
|
||||
$ret['results'] = array();
|
||||
foreach ($items as $id) {
|
||||
++$i;
|
||||
if ($i > $max) {
|
||||
break;
|
||||
}
|
||||
if ($i > $min) {
|
||||
$ret['results'][$id] = array(
|
||||
'id' => $id,
|
||||
'score' => 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
public function fieldsUpdated(SearchApiIndex $index) {
|
||||
return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0;
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user