first import 7.x-1.4

This commit is contained in:
bachy 2013-03-15 17:32:30 +01:00
commit 9cc5ba4cfa
68 changed files with 17899 additions and 0 deletions

338
CHANGELOG.txt Normal file
View 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
View 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
View 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

View 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")

View 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;
}
}

View 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(),
);
}
}
}

View 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;
}
}

View 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;
}
}

View 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".
*/

View 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"

View 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');
}

View 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);
}

View 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.

View 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;
}
}

View 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));
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View 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));
}
}

View 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']);
}
}
}
}

View 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 : '',
);
}
}

View 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']);
}
}
}
}

View 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;
}
}

View File

@ -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();
}
}

View 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']);
}
}
}
}
}
}

View 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"));
}
}

View 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']);
}
}

View 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;
}
}

View 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"

View 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');
}

View 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');
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

BIN
enabled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

220
includes/callback.inc Normal file
View 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();
}
}

View 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'];
}

View 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);
}
}
}
}

View 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',
),
);
}
}

View 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',
),
);
}
}

View 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']);
}
}

View 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]);
}
}
}
}
}

View 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);
}
}

View 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
View 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);
}
}
}

View 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));
}
}

View 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
View 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
View 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
View 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) {
}
}

View 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 &lt;@tag&gt; can't be an array.", array('@tag' => $key));
}
elseif (!is_numeric($value)) {
$errors[] = t("Boost value for tag &lt;@tag&gt; must be numeric.", array('@tag' => $key));
}
elseif ($value < 0) {
$errors[] = t('Boost value for tag &lt;@tag&gt; 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;
}
}

View File

@ -0,0 +1,12 @@
<?php
/**
* Processor for making searches case-insensitive.
*/
class SearchApiIgnoreCase extends SearchApiAbstractProcessor {
protected function process(&$value) {
$value = drupal_strtolower($value);
}
}

View 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;
}
}

View 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

File diff suppressed because it is too large Load Diff

228
includes/server_entity.inc Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

61
search_api.admin.js Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

90
search_api.rules.inc Normal file
View 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
View 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>: &lt;a href="URL" title="MOUSEOVER TEXT"&gt;TEXT&lt;/a&gt;.
&lt; signs can be <A HREF="http://example.com/topic/html-escapes" TITLE = 'HTML &quot;escapes&quot;'
TARGET = '_blank'>escaped</A> with "&amp;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' => '"&lt;"', '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.'));
}
}

View 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"

View 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;
}

View 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;
}
}