From 5265c92c5518ba2db870d9ca24562ac017d2057a Mon Sep 17 00:00:00 2001
From: David Maus <maus@hab.de>
Date: Thu, 7 Feb 2013 11:24:17 +0100
Subject: [PATCH] Import VuFindSearch module

---
 module/VuFindSearch/LICENSE                   | 278 ++++++++
 module/VuFindSearch/Module.php                | 110 +++
 module/VuFindSearch/config/.keep              |   0
 .../VuFindSearch/Backend/BackendInterface.php | 103 +++
 .../Backend/Exception/BackendException.php    |  44 ++
 .../Exception/RemoteErrorException.php        |  45 ++
 .../Exception/RequestErrorException.php       |  45 ++
 .../src/VuFindSearch/Backend/Solr/Backend.php | 350 ++++++++++
 .../VuFindSearch/Backend/Solr/Connector.php   | 459 ++++++++++++
 .../Backend/Solr/QueryBuilder.php             | 653 ++++++++++++++++++
 .../Backend/Solr/Response/Json/Record.php     | 108 +++
 .../Solr/Response/Json/RecordCollection.php   | 243 +++++++
 .../Response/Json/RecordCollectionFactory.php |  93 +++
 .../Backend/Solr/SearchHandler.php            | 448 ++++++++++++
 .../Exception/ExceptionInterface.php          |  43 ++
 .../Exception/InvalidArgumentException.php    |  42 ++
 .../Exception/RuntimeException.php            |  43 ++
 .../src/VuFindSearch/ParamBag.php             | 199 ++++++
 .../src/VuFindSearch/Query/AbstractQuery.php  |  42 ++
 .../src/VuFindSearch/Query/Params.php         | 269 ++++++++
 .../src/VuFindSearch/Query/Query.php          | 120 ++++
 .../src/VuFindSearch/Query/QueryAdapter.php   |  71 ++
 .../src/VuFindSearch/Query/QueryGroup.php     | 210 ++++++
 .../RecordCollectionFactoryInterface.php      |   8 +
 .../Response/RecordCollectionInterface.php    | 115 +++
 .../VuFindSearch/Response/RecordInterface.php |  62 ++
 .../VuFindSearch/src/VuFindSearch/Service.php | 296 ++++++++
 .../tests/unit-tests/bootstrap.php            |  24 +
 .../unit-tests/fixtures/searchspecs.yaml      | 495 +++++++++++++
 .../fixtures/solr/response/bad-request        |   1 +
 .../solr/response/internal-server-error       |   1 +
 .../fixtures/solr/response/no-match           |   6 +
 .../fixtures/solr/response/single-record      |   6 +
 .../VuFindSearch/tests/unit-tests/phpunit.xml |  12 +
 .../VuFindSearch/tests/unit-tests/src/.keep   |   0
 .../VuFindTest/Backend/Solr/BackendTest.php   |  88 +++
 .../VuFindTest/Backend/Solr/ConnectorTest.php | 143 ++++
 .../Backend/Solr/QueryBuilderTest.php         | 148 ++++
 .../Json/RecordCollectionFactoryTest.php      |  58 ++
 .../Backend/Solr/SearchHandlerTest.php        |  57 ++
 40 files changed, 5538 insertions(+)
 create mode 100644 module/VuFindSearch/LICENSE
 create mode 100644 module/VuFindSearch/Module.php
 create mode 100644 module/VuFindSearch/config/.keep
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/ParamBag.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/Params.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/Query.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php
 create mode 100644 module/VuFindSearch/src/VuFindSearch/Service.php
 create mode 100644 module/VuFindSearch/tests/unit-tests/bootstrap.php
 create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml
 create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request
 create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error
 create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match
 create mode 100644 module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record
 create mode 100644 module/VuFindSearch/tests/unit-tests/phpunit.xml
 create mode 100644 module/VuFindSearch/tests/unit-tests/src/.keep
 create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php
 create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php
 create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php
 create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php
 create mode 100644 module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php

diff --git a/module/VuFindSearch/LICENSE b/module/VuFindSearch/LICENSE
new file mode 100644
index 00000000000..4eb3f1e9596
--- /dev/null
+++ b/module/VuFindSearch/LICENSE
@@ -0,0 +1,278 @@
+		    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.
diff --git a/module/VuFindSearch/Module.php b/module/VuFindSearch/Module.php
new file mode 100644
index 00000000000..f2406e40d2f
--- /dev/null
+++ b/module/VuFindSearch/Module.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * ZF2 module definition for the VF2 search service.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch;
+
+use Zend\ServiceManager\ServiceManager;
+use Zend\EventManager\EventManager;
+
+/**
+ * ZF2 module definition for the VF2 search service.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class Module
+{
+
+    /**
+     * Return autoloader configuration.
+     *
+     * @return array
+     */
+    public function getAutoloaderConfig ()
+    {
+        return array(
+            'Zend\Loader\StandardAutoloader' => array(
+                'namespaces' => array(
+                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
+                ),
+            ),
+        );
+    }
+
+    /**
+     * Return service configuration.
+     *
+     * @return array
+     */
+    public function getServiceConfig ()
+    {
+        return array(
+            'factories' => array(
+                'VuFind\Search' => array($this, 'setup'),
+            )
+        );
+    }
+
+    /**
+     * Return configured search service to superior service manager.
+     *
+     * @param ServiceManager $sm Service manager
+     *
+     * @return SearchService
+     */
+    public function setup (ServiceManager $sm)
+    {
+        $service = new Service();
+        if ($sm->has('VuFind\Logger')) {
+            $service->setLogger($sm->get('VuFind\Logger'));
+        }
+        return $service;
+    }
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/config/.keep b/module/VuFindSearch/config/.keep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php b/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php
new file mode 100644
index 00000000000..2ee42f57844
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * Search backend interface definition.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend;
+
+use VuFindSearch\Query\AbstractQuery;
+use VuFindSearch\Query\QueryGroup;
+use VuFindSearch\Query\Query;
+
+use VuFindSearch\Query\Params;
+
+/**
+ * Search backend interface definition.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+interface BackendInterface
+{
+
+    /**
+     * Perform a search and return record collection.
+     *
+     * @param AbstractQuery $query  Search query
+     * @param Params        $params Search parameters
+     *
+     * @return RecordCollectionInterface
+     */
+    public function search (AbstractQuery $query, Params $params);
+
+    /**
+     * Retrieve a single document.
+     *
+     * @param string $id Document identifier
+     *
+     * @return RecordCollectionInterface
+     */
+    public function retrieve ($id);
+
+    /**
+     * Delete a single record.
+     *
+     * @param string $id Record identifier
+     *
+     * @return RecordCollectionInterface
+     */
+    public function delete ($id);
+
+    /**
+     * Return similar records.
+     *
+     * @param string $id Id of record to compare with
+     *
+     * @return RecordCollectionInterface
+     */
+    public function similar ($id);
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php
new file mode 100644
index 00000000000..5b8ecfe185a
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * BackendException.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Exception;
+
+use VuFindSearch\Exception\RuntimeException;
+
+/**
+ * BackendException.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class BackendException extends RuntimeException
+{}
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php
new file mode 100644
index 00000000000..acc7755809b
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * RemoteErrorException.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Exception;
+
+/**
+ * RemoteErrorException.
+ *
+ * Signal an exceptional error of the remote backend service, e.g. indicated
+ * by a 5xx response code.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class RemoteErrorException extends BackendException
+{}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php
new file mode 100644
index 00000000000..0ddc78194c4
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * RequestErrorException.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Exception;
+
+/**
+ * RequestErrorException.
+ *
+ * Signals an error in the request to the remote service, e.g. indicated by a
+ * 4xx response code.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class RequestErrorException extends BackendException
+{}
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php
new file mode 100644
index 00000000000..566c1fd317b
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php
@@ -0,0 +1,350 @@
+<?php
+
+/**
+ * SOLR backend.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr;
+
+use VuFindSearch\Query\AbstractQuery;
+use VuFindSearch\Query\QueryGroup;
+use VuFindSearch\Query\Query;
+use VuFindSearch\Query\Params;
+
+use VuFindSearch\Response\RecordCollectionInterface;
+use VuFindSearch\Response\RecordCollectionFactoryInterface;
+
+use VuFindSearch\Backend\BackendInterface;
+
+use Zend\Log\LoggerInterface;
+
+use VuFindSearch\Backend\Exception\BackendException;
+
+/**
+ * SOLR backend.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class Backend implements BackendInterface
+{
+    /**
+     * Record collection factory.
+     *
+     * @var RecordCollectionFactoryInterface
+     */
+    protected $collectionFactory;
+
+    /**
+     * Dictionaries for spellcheck.
+     *
+     * @var array
+     */
+    protected $dictionaries;
+
+    /**
+     * Logger, if any.
+     *
+     * @var LoggerInterface
+     */
+    protected $logger;
+
+    /**
+     * Connector.
+     *
+     * @var Connector
+     */
+    protected $connector;
+
+    /**
+     * Backend identifier.
+     *
+     * @var string
+     */
+    protected $identifier;
+
+    /**
+     * Query builder.
+     *
+     * @var QueryBuilder
+     */
+    protected $queryBuilder;
+
+    /**
+     * Constructor.
+     *
+     * @param string    $identifier Backend identifier
+     * @param Connector $connector  SOLR connector
+     *
+     * @return void
+     */
+    public function __construct ($identifier, Connector $connector)
+    {
+        $this->connector    = $connector;
+        $this->identifier   = $identifier;
+        $this->dictionaries = array();
+    }
+
+    /**
+     * Set the spellcheck dictionaries to use.
+     *
+     * @param array $dictionaries Spellcheck dictionaries
+     *
+     * @return void
+     */
+    public function setDictionaries (array $dictionaries)
+    {
+        $this->dictionaries = $dictionaries;
+    }
+
+    /**
+     * Perform a search and return record collection.
+     *
+     * @param AbstractQuery $query  Search query
+     * @param Params        $params Search parameters
+     *
+     * @return RecordCollectionInterface
+     *
+     * @todo Disable more SOLR request options when resubmitting for spellcheck
+     * @todo Implement merge of spellcheck results
+     */
+    public function search (AbstractQuery $query, Params $params)
+    {
+        if ($params->isSpellcheckEnabled()) {
+            if (!empty($this->dictionaries)) {
+                reset($this->dictionaries);
+                $params->setSpellcheckDictionary(current($this->dictionaries));
+            } else {
+                $this->log('warn', 'Spellcheck requested but no spellcheck dictionary configured');
+            }
+        }
+
+        $response   = $this->connector->search($query, $params, $this->getQueryBuilder());
+        $collection = $this->getRecordCollectionFactory()->factory($this->deserialize($response));
+        $this->injectSourceIdentifier($collection);
+
+        // Submit requests for more spelling suggestions
+        while (next($this->dictionaries) !== false) {
+            $req = $this->connector->getLastRequestParameters();
+            $req->set('spellcheck.dictionary', array(current($this->dictionaries)));
+            $req->set('rows', array(0));
+            $response = $this->connector->resubmit();
+        }
+
+        return $collection;
+    }
+
+    /**
+     * Retrieve a single document.
+     *
+     * @param string $id Document identifier
+     *
+     * @return RecordCollectionInterface
+     */
+    public function retrieve ($id)
+    {
+        $response   = $this->connector->retrieve($id);
+        $collection = $this->getRecordCollectionFactory()->factory($this->deserialize($response));
+        $this->injectSourceIdentifier($collection);
+        return $collection;
+    }
+
+    /**
+     * Return similar records.
+     *
+     * @param string $id Id of record to compare with
+     *
+     * @return RecordCollectionInterface
+     */
+    public function similar ($id)
+    {
+        $response   = $this->connector->similar($id);
+        $collection = $this->getRecordCollectionFactory()->factory($this->deserialize($response));
+        $this->injectSourceIdentifier($collection);
+        return $collection;
+    }
+
+    /**
+     * Delete a single record.
+     *
+     * @param string $id Record identifier
+     *
+     * @return void
+     *
+     * @todo Currently not implemented in the connector
+     */
+    public function delete ($id)
+    {
+        $this->connector->delete($id);
+    }
+
+    /**
+     * Set the Logger.
+     *
+     * @param LoggerInterface $logger Logger
+     *
+     * @return void
+     */
+    public function setLogger (LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Return query builder.
+     *
+     * Lazy loads an empty QueryBuilder if none was set.
+     *
+     * @return QueryBuilder
+     */
+    public function getQueryBuilder ()
+    {
+        if (!$this->queryBuilder) {
+            $this->queryBuilder = new QueryBuilder();
+        }
+        return $this->queryBuilder;
+    }
+
+    /**
+     * Set the query builder.
+     *
+     * @param QueryBuilder $queryBuilder Query builder
+     *
+     * @return void
+     *
+     * @todo Typehint QueryBuilderInterface
+     */
+    public function setQueryBuilder (QueryBuilder $queryBuilder)
+    {
+        $this->queryBuilder = $queryBuilder;
+    }
+
+    /**
+     * Return backend identifier.
+     *
+     * @return string
+     */
+    public function getIdentifier ()
+    {
+        return $this->identifier;
+    }
+
+    /**
+     * Set the record collection factory.
+     *
+     * @param RecordCollectionFactoryInterface $factory Factory
+     *
+     * @return void
+     */
+    public function setRecordCollectionFactory (RecordCollectionFactoryInterface $factory)
+    {
+        $this->collectionFactory = $factory;
+    }
+
+    /**
+     * Return the record collection factory.
+     *
+     * Lazy loads a generic collection factory.
+     *
+     * @return RecordCollectionFactoryInterface
+     */
+    public function getRecordCollectionFactory ()
+    {
+        if (!$this->collectionFactory) {
+            $this->collectionFactory = new Response\Json\RecordCollectionFactory();
+        }
+        return $this->collectionFactory;
+    }
+
+    /**
+     * Return the SOLR connector.
+     *
+     * @return ConnectorInterface
+     */
+    public function getConnector ()
+    {
+        return $this->connector;
+    }
+
+    /// Internal API
+
+    /**
+     * Inject source identifier in record collection and all contained records.
+     *
+     * @param ResponseInterface $response Response
+     *
+     * @return void
+     */
+    protected function injectSourceIdentifier (RecordCollectionInterface $response)
+    {
+        $response->setSourceIdentifier($this->identifier);
+        foreach ($response as $record) {
+            $record->setSourceIdentifier($this->identifier);
+        }
+        return $response;
+    }
+
+    /**
+     * Send a message to the logger.
+     *
+     * @param string $level   Log level
+     * @param string $message Log message
+     * @param array  $context Log context
+     *
+     * @return void
+     */
+    protected function log ($level, $message, array $context = array())
+    {
+        if ($this->logger) {
+            $this->logger->$level($message, $context);
+        }
+    }
+
+    /**
+     * Deserialize JSON response.
+     *
+     * @param string $json Serialized JSON response
+     *
+     * @return array
+     *
+     * @throws BackendException Deserialization error
+     */
+    protected function deserialize ($json)
+    {
+        $response = json_decode($json, true);
+        $error    = json_last_error();
+        if ($error != \JSON_ERROR_NONE) {
+            throw new RuntimeException(
+                sprintf('JSON decoding error: %s', $error)
+            );
+        }
+        return $response;
+    }
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php
new file mode 100644
index 00000000000..61178acadf1
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php
@@ -0,0 +1,459 @@
+<?php
+
+/**
+ * SOLR connector.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr;
+
+use VuFindSearch\Query\AbstractQuery;
+use VuFindSearch\Query\QueryGroup;
+use VuFindSearch\Query\Query;
+
+use VuFindSearch\ParamBag;
+use VuFindSearch\Query\Params;
+
+use VuFindSearch\Backend\Exception\RemoteErrorException;
+use VuFindSearch\Backend\Exception\RequestErrorException;
+
+use Zend\Http\Request;
+use Zend\Http\Client as HttpClient;
+
+use Zend\Log\LoggerInterface;
+
+/**
+ * SOLR connector.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class Connector
+{
+    /**
+     * Maximum length of a GET url.
+     *
+     * Switches to POST if the SOLR target URL exeeds this length.
+     *
+     * @see self::sendRequest()
+     *
+     * @var integer
+     */
+    const MAX_GET_URL_LENGTH = 2048;
+
+    /**
+     * Logger instance.
+     *
+     * @var LoggerInterface
+     */
+    protected $logger;
+
+    /**
+     * URL of SOLR core.
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * HTTP read timeout.
+     *
+     * @var float
+     */
+    protected $timeout = 30;
+
+    /**
+     * Proxy service
+     *
+     * @var mixed
+     */
+    protected $proxy;
+
+    /**
+     * Query invariants.
+     *
+     * @var ParamBag
+     */
+    protected $invariants;
+
+    /**
+     * Last request.
+     *
+     * @see self::resubmit()
+     *
+     * @var array
+     */
+    protected $lastRequest;
+
+    /**
+     * Class of HTTP client adapter to use.
+     *
+     * @see self::sendRequest()
+     *
+     * @var string
+     */
+    protected $httpAdapterClass = 'Zend\Http\Client\Adapter\Socket';
+
+    /**
+     * Constructor
+     *
+     * @param string $url SOLR base URL
+     *
+     * @return void
+     */
+    public function __construct($url)
+    {
+        $this->url = $url;
+    }
+
+    /// Public API
+
+    /**
+     * Return document specified by id.
+     *
+     * @param string $id The document to retrieve from Solr
+     *
+     * @return string
+     */
+    public function retrieve ($id)
+    {
+        $params = new ParamBag();
+        $params->set('q', sprintf('id:"%s"', addcslashes($id, '"')));
+        $result = $this->select($params);
+        return $result;
+    }
+
+    /**
+     * Return records similar to a given record specified by id.
+     *
+     * Uses MoreLikeThis Request Handler
+     *
+     * @param string $id Id of given record
+     *
+     * @return string
+     */
+    public function similar ($id)
+    {
+        $params = new ParamBag();
+        $params->set('q', sprintf('id:"%s"', addcslashes($id, '"')));
+        $params->set('qt', 'morelikethis');
+        return $this->select($params);
+    }
+
+    /**
+     * Execute a search.
+     *
+     * @param AbstractQuery $query        Search query
+     * @param Params        $params       Search parameters
+     * @param QueryBuilder  $queryBuilder Query builder
+     *
+     * @return string
+     */
+    public function search (AbstractQuery $query, Params $params, QueryBuilder $queryBuilder)
+    {
+        $queryParams = new ParamBag();
+        $queryParams->set('start', $params->getOffset());
+        $queryParams->set('rows', $params->getLimit());
+        $queryParams->set('fl', '*,score');
+        $queryParams->set('sort', $params->getSort());
+
+        $queryParams->mergeWith($queryBuilder->build($query, $params));
+
+        return $this->select($queryParams);
+    }
+
+    /**
+     * Return the current query invariants.
+     *
+     * @return ParamBag
+     */
+    public function getQueryInvariants ()
+    {
+        if (!$this->invariants) {
+            $this->invariants = new ParamBag(array('wt' => 'json', 'json.nl' => 'arrarr'));
+        }
+        return $this->invariants;
+    }
+
+    /**
+     * Set the query invariants.
+     *
+     * @param array $invariants Query invariants
+     *
+     * @return void
+     */
+    public function setQueryInvariants (array $invariants)
+    {
+        $this->invariants = new ParamBag($invariants);
+    }
+
+    /**
+     * Add a query invariant.
+     *
+     * @param string $parameter Query parameter
+     * @param string $value     Query parameter value
+     *
+     * @return void
+     */
+    public function addQueryInvariant ($parameter, $value)
+    {
+        $this->getQueryInvariants()->add($parameter, $value);
+    }
+
+    /**
+     * Set logger instance.
+     *
+     * @param LoggerInterface $logger Logger
+     *
+     * @return void
+     */
+    public function setLogger (LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Return parameters of the last request.
+     *
+     * @return ParamBag
+     */
+    public function getLastRequestParameters ()
+    {
+        return $this->lastRequest['parameters'];
+    }
+
+    /**
+     * Set the HTTP proxy service.
+     *
+     * @param mixed $proxy Proxy service
+     *
+     * @return void
+     *
+     * @todo Typehint on ProxyInterface
+     */
+    public function setProxy ($proxy)
+    {
+        $this->proxy = $proxy;
+    }
+
+    /**
+     * Set the HTTP connect timeout.
+     *
+     * @param float $timeout Timeout in seconds
+     *
+     * @return void
+     */
+    public function setTimeout ($timeout)
+    {
+        $this->timeout = $timeout;
+    }
+
+    /**
+     * Set adapter class of HTTP client.
+     *
+     * Keep in mind that a proxy service might replace the client adapter by a
+     * Proxy adapter if necessary.
+     *
+     * @param string $adapterClass Name of adapter class
+     *
+     * @return void
+     */
+    public function setHttpAdapterClass ($adapterClass = 'Zend\Http\Client\Adapter\Socket')
+    {
+        $this->httpAdapterClass = $adapterClass;
+    }
+
+    /// Internal API
+
+    /**
+     * Send request to `select' query handler and return response.
+     *
+     * @param ParamBag $params Request parameters
+     *
+     * @return array
+     */
+    protected function select (ParamBag $params)
+    {
+        $this->prepare($params);
+        $result = $this->sendRequest('select', $params);
+        return $result;
+    }
+
+    /**
+     * Obtain information from an alphabetic browse index.
+     *
+     * @param string $source    Name of index to search
+     * @param string $from      Starting point for browse results
+     * @param int    $page      Result page to return (starts at 0)
+     * @param int    $page_size Number of results to return on each page
+     *
+     * @return string
+     *
+     * @todo Deserialize and process the response in backend
+     */
+    public function alphabeticBrowse ($source, $from, $page, $page_size = 20)
+    {
+        $params = new ParamBag();
+        $params->set('from', $from);
+        $params->set('offset', $page * $page_size);
+        $params->set('rows', $page_size);
+        $params->set('source', $source);
+
+        $url = $this->url . '/browse';
+
+        $this->prepare($params);
+
+        $result = $this->sendRequest('browse', $params);
+        return $result;
+    }
+
+    /**
+     * Extract terms from the Solr index.
+     *
+     * @param string $field Field to extract terms from
+     * @param string $start Starting term to extract (blank for beginning of list)
+     * @param int    $limit Maximum number of terms to return (-1 for no limit)
+     *
+     * @return string
+     *
+     * @todo Deserialize and process the response in backend
+     */
+    public function getTerms ($field, $start, $limit)
+    {
+        $params = new ParamBag();
+        $params->set('terms', 'true');
+        $params->set('terms.lower.incl', 'false');
+        $params->set('terms.fl', $field);
+        $params->set('terms.lower', $start);
+        $params->set('terms.limit', $limit);
+        $params->set('terms.sort', 'index');
+
+        $this->prepare($params);
+
+        $result = $this->sendRequest('term', $params);
+        return $result;
+    }
+
+    /// Internal API
+
+    /**
+     * Prepare final request parameters.
+     *
+     * This function is called right before the request is send. Adds the
+     * invariants of our SOLR queries.
+     *
+     * @param ParamBag $params Parameters
+     *
+     * @return void
+     */
+    protected function prepare (ParamBag $params)
+    {
+        $params->mergeWith($this->getQueryInvariants());
+        return;
+    }
+
+    /**
+     * Repeat the last request with potentially modified parameters.
+     *
+     * @see self::getLastRequestParameters()
+     *
+     * @return void
+     */
+    public function resubmit ()
+    {
+        $last = $this->lastRequest;
+        return $this->sendRequest($last['handler'], $last['parameters'], $last['method']);
+    }
+
+    /**
+     * Send request to SOLR and return the response.
+     *
+     * @param string   $handler SOLR request handler to use
+     * @param ParamBag $params  Request parameters
+     * @param string   $method  Request method
+     *
+     * @return Zend\Http\Response
+     *
+     * @throws RemoteErrorException  SOLR signaled a server error (HTTP 5xx)
+     * @throws RequestErrorException SOLR signaled a client error (HTTP 4xx)
+     */
+    protected function sendRequest ($handler, ParamBag $params, $method = Request::METHOD_GET)
+    {
+        $client = new HttpClient();
+        $client->setAdapter($this->httpAdapterClass);
+        $client->setMethod($method);
+        $client->setOptions(array('timeout' => $this->timeout));
+
+        $url    = $this->url . '/' . $handler;
+
+        $paramString = implode('&', $params->request());
+        if (strlen($paramString) > self::MAX_GET_URL_LENGTH) {
+            $method = Request::METHOD_POST;
+        }
+
+        if ($method === Request::METHOD_POST) {
+            $client->setUri($url);
+            $client->setRawBody($paramString);
+            $client->setHeaders(array('Content-Type' => Request::ENC_URLENCODED, 'Content-Length' => strlen($paramString)));
+        } else {
+            $url = $url . '?' . $paramString;
+            $client->setUri($url);
+        }
+
+        if ($this->proxy) {
+            $this->proxy->proxify($client);
+        }
+
+        if ($this->logger) {
+            $this->logger->debug(sprintf('=> %s %s', $client->getMethod(), $client->getUri()), array('params' => $params->request()));
+        }
+
+        $this->lastRequest = array('parameters' => $params, 'handler' => $handler, 'method' => $method);
+
+        $response = $client->send();
+
+        if ($this->logger) {
+            $this->logger->debug(sprintf('<= %s %s', $response->getStatusCode(), $response->getReasonPhrase()));
+        }
+
+        if (!$response->isSuccess()) {
+            $status = $response->getStatusCode();
+            $phrase = $response->getReasonPhrase();
+            if ($status >= 500) {
+                throw new RemoteErrorException($phrase, $status);
+            } else {
+                throw new RequestErrorException($phrase, $status);
+            }
+        }
+        return $response->getBody();
+    }
+}
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php
new file mode 100644
index 00000000000..20fbdfa5a0a
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php
@@ -0,0 +1,653 @@
+<?php
+
+/**
+ * SOLR QueryBuilder.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr;
+
+use VuFindSearch\Query\AbstractQuery;
+use VuFindSearch\Query\QueryGroup;
+use VuFindSearch\Query\Query;
+
+use VuFindSearch\ParamBag;
+use VuFindSearch\Query\Params;
+
+/**
+ * SOLR QueryBuilder.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class QueryBuilder
+{
+
+    /**
+     * Regular expression matching a SOLR range.
+     *
+     * @var string
+     */
+    const SOLR_RANGE_RE = '/(\[.+\s+TO\s+.+\])|(\{.+\s+TO\s+.+\})/';
+
+    /**
+     * Lookahead that detects whether or not we are inside quotes.
+     *
+     * @var string
+     */
+    protected static $insideQuotes = '(?=(?:[^\"]*+\"[^\"]*+\")*+[^\"]*+$)';
+
+    /**
+     * Search specs.
+     *
+     * @var array
+     */
+    protected $specs;
+
+    /**
+     * Force ranges to uppercase?
+     *
+     * @var boolean
+     */
+    public $caseSensitiveRanges = true;
+
+    /**
+     * Force boolean operators to uppercase?
+     *
+     * @var boolean
+     */
+    public $caseSensitiveBooleans = true;
+
+    /**
+     * Constructor.
+     *
+     * @param array $specs Search handler specifications
+     *
+     * @return void
+     */
+    public function __construct (array $specs = array())
+    {
+        $this->setSpecs($specs);
+    }
+
+    /// Public API
+
+    /**
+     * Return SOLR search parameters based on a user query and params.
+     *
+     * @param AbstractQuery $query  User query
+     * @param Params        $params User query params
+     *
+     * @return ParamBag
+     *
+     * @todo Review usage of filters
+     * @todo Review usage of facets
+     * @todo Implement hightlighting
+     */
+    public function build (AbstractQuery $query, Params $params)
+    {
+
+        if ($query instanceOf QueryGroup) {
+            $query = $this->reduceQueryGroup($query);
+        } else {
+            $query->setString($this->normalizeSearchString($query->getString()));
+        }
+
+        $searchParams = new ParamBag();
+
+        // Filters
+        foreach ($params->getFilters() as $field => $values) {
+            foreach ($values as $value) {
+                // Allow trailing wildcards and ranges
+                if (substr($value, -1) == '*' || preg_match(self::SOLR_RANGE_RE, $value)) {
+                    $searchParams->add('fq', "{$field}:{$value}");
+                } else {
+                    $searchParams->add('fq', sprintf('%s:"%s"', $field, addcslashes($value, '"')));
+                }
+            }
+        }
+
+        // Facet settings
+        $facets = $params->getFacets();
+        if (!empty($facets)) {
+            $searchParams->set('facet', 'true');
+            foreach ($facets as $name => $value) {
+                $solrName = "facet.{$name}";
+                $searchParams->set($solrName, $value);
+            }
+        }
+
+        $searchParams->mergeWith(
+            $this->createSearchParams($query)
+        );
+        return $searchParams;
+    }
+
+    /**
+     * Return true if the search string contains advanced Lucene syntax.
+     *
+     * @param string $searchString Search string
+     *
+     * @return boolean
+     *
+     * @todo Maybe factor out to dedicated UserQueryAnalyzer
+     */
+    public function containsAdvancedLuceneSyntax ($searchString)
+    {
+        // Check for various conditions that flag an advanced Lucene query:
+        if ($searchString == '*:*') {
+            return true;
+        }
+
+        // The following conditions do not apply to text inside quoted strings,
+        // so let's just strip all quoted strings out of the query to simplify
+        // detection.  We'll replace quoted phrases with a dummy keyword so quote
+        // removal doesn't interfere with the field specifier check below.
+        $searchString = preg_replace('/"[^"]*"/', 'quoted', $searchString);
+
+        // Check for field specifiers:
+        if (preg_match("/[^\s]\:[^\s]/", $searchString)) {
+            return true;
+        }
+
+        // Check for parentheses and range operators:
+        if (strstr($searchString, '(') && strstr($searchString, ')')) {
+            return true;
+        }
+        $rangeReg = self::SOLR_RANGE_RE;
+        if (!$this->caseSensitiveRanges) {
+            $rangeReg .= "i";
+        }
+        if (preg_match($rangeReg, $searchString)) {
+            return true;
+        }
+
+        // Build a regular expression to detect booleans -- AND/OR/NOT surrounded
+        // by whitespace, or NOT leading the query and followed by whitespace.
+        $boolReg = '/((\s+(AND|OR|NOT)\s+)|^NOT\s+)/';
+        if (!$this->caseSensitiveBooleans) {
+            $boolReg .= "i";
+        }
+        if (preg_match($boolReg, $searchString)) {
+            return true;
+        }
+
+        // Check for wildcards and fuzzy matches:
+        if (strstr($searchString, '*') || strstr($searchString, '?') || strstr($searchString, '~')) {
+            return true;
+        }
+
+        // Check for boosts:
+        if (preg_match('/[\^][0-9]+/', $searchString)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Set query builder search specs.
+     *
+     * @param array $specs Search specs
+     *
+     * @return void
+     */
+    public function setSpecs (array $specs)
+    {
+        foreach ($specs as $handler => $spec) {
+            $this->specs[strtolower($handler)] = new SearchHandler($spec);
+        }
+    }
+
+    /**
+     * Return search specs.
+     *
+     * @return array
+     */
+    public function getSpecs ()
+    {
+        $specs = array();
+        foreach ($specs as $handler => $spec) {
+            $specs[$handler] = $spec->toArray();
+        }
+        return $specs;
+    }
+
+    /// Internal API
+
+    /**
+     * Return ParamBag with search query related parameters.
+     *
+     * @param Query $query User query
+     *
+     * @return ParamBag
+     *
+     * @todo Review highlighting
+     */
+    protected function createSearchParams (Query $query)
+    {
+        $string  = $query->getString() ?: '*:*';
+        $handler = $this->getSearchHandler($query->getHandler());
+
+        $params  = new ParamBag();
+
+        if ($this->containsAdvancedLuceneSyntax($string)) {
+
+            if ($handler) {
+                // No need to check if hl is enabled or not; hl.q w/o hl=true has no effect
+                //   -- dmaus, 20121107
+                $params->set('hl.q', $this->createAdvancedInnerSearchString($string, $handler));
+                $string = $this->createAdvancedInnerSearchString($string, $handler);
+                if ($handler->hasDismax()) {
+                    $string = $handler->createBoostQueryString($string);
+                }
+            }
+        } else {
+            if ($handler && $handler->hasDismax()) {
+                $params->set('qf', implode(' ', $handler->getDismaxFields()));
+                $params->set('qt', 'dismax');
+                foreach ($handler->getDismaxParams() as $param) {
+                    foreach ($param as $pair) {
+                        $params->add(reset($pair), next($pair));
+                    }
+                }
+                if ($handler->hasFilterQuery()) {
+                    $params->add('fq', $handler->getFilterQuery());
+                }
+            } else {
+                if ($handler) {
+                    $string = $handler->createSimpleQueryString($string);
+                }
+            }
+        }
+        $params->set('q', $string);
+        return $params;
+    }
+
+    /**
+     * Return named search handler.
+     *
+     * @param string $handler Search handler name
+     *
+     * @return SearchHandler|null
+     */
+    protected function getSearchHandler ($handler)
+    {
+        if ($handler && isset($this->specs[$handler])) {
+            return $this->specs[$handler];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Reduce query group a single query.
+     *
+     * @param QueryGroup $group Query group to reduce
+     *
+     * @return Query
+     */
+    protected function reduceQueryGroup (QueryGroup $group)
+    {
+        $searchString  = $this->reduceQueryGroupComponents($group);
+        $searchHandler = $group->getReducedHandler();
+        return new Query($searchString, $searchHandler);
+    }
+
+    /**
+     * Reduce components of query group to a search string of a simple query.
+     *
+     * This function implements the recursive reduction of a query group.
+     *
+     * @param AbstractQuery $component Component
+     *
+     * @return string
+     *
+     * @see self::reduceQueryGroup()
+     *
+     */
+    protected function reduceQueryGroupComponents (AbstractQuery $component)
+    {
+        if ($component instanceOf QueryGroup) {
+            $reduced = array_map(array($this, 'reduceQueryGroupComponents'), $component->getQueries());
+            $searchString = $component->isNegated() ? 'NOT ' : '';
+            $searchString .= sprintf('(%s)', implode(" {$component->getOperator()} ", $reduced));
+        } else {
+            $searchString  = $this->normalizeSearchString($component->getString());
+            $searchHandler = $this->getSearchHandler($component->getHandler());
+            if ($searchHandler) {
+                $searchString = $this->createSearchString($searchString, $searchHandler);
+            }
+        }
+        return $searchString;
+    }
+
+
+    /**
+     * Return search string based on input and handler.
+     *
+     * @param string        $string  Input search string
+     * @param SearchHandler $handler Search handler
+     *
+     * @return string
+     *
+     */
+    protected function createSearchString ($string, SearchHandler $handler = null)
+    {
+        $advanced = $this->containsAdvancedLuceneSyntax($string);
+
+        if ($advanced && $handler) {
+            return $handler->createAdvancedQueryString($string);
+        } else if ($handler) {
+            return $handler->createSimpleQueryString($string);
+        } else {
+            return $string;
+        }
+    }
+
+    /**
+     * Return advanced inner search string based on input and handler.
+     *
+     * @param string        $string  Input search string
+     * @param SearchHandler $handler Search handler
+     *
+     * @return string
+     *
+     */
+    protected function createAdvancedInnerSearchString ($string, SearchHandler $handler)
+    {
+        // Special case -- if the user wants all records but the current handler
+        // has a filter query, apply the filter query:
+        if (trim($string) === '*:*' && $handler && $handler->hasFilterQuery()) {
+            return $handler->getFilterQuery();
+        }
+
+        // Strip out any colons that are NOT part of a field specification:
+        $string = preg_replace('/(\:\s+|\s+:)/', ' ', $string);
+
+        // If the query already includes field specifications, we can't easily
+        // apply it to other fields through our defined handlers, so we'll leave
+        // it as-is:
+        if (strstr($string, ':')) {
+            return $string;
+        }
+
+        // Convert empty queries to return all values in a field:
+        if (empty($string)) {
+            $string = '[* TO *]';
+        }
+
+        // If the query ends in a question mark, the user may not really intend to
+        // use the question mark as a wildcard -- let's account for that possibility
+        if (substr($string, -1) == '?') {
+            $string = "({$string}) OR (" . substr($string, 0, strlen($string) - 1) . ")";
+        }
+
+        return $handler ? $handler->createAdvancedQueryString($string, false) : $string;
+    }
+
+    /**
+     * Return normalized input string.
+     *
+     * @param string $searchString Input search string
+     *
+     * @return string
+     */
+    protected function normalizeSearchString ($searchString)
+    {
+        $searchString = $this->prepareForLuceneSyntax($searchString);
+
+        // Force boolean operators to uppercase if we are in a
+        // case-insensitive mode:
+        if (!$this->caseSensitiveBooleans) {
+            $searchString = $this->capitalizeBooleans($searchString);
+        }
+        // Adjust range operators if we are in a case-insensitive mode:
+        if (!$this->caseSensitiveRanges) {
+            $searchString = $this->capitalizeRanges($searchString);
+        }
+        return $searchString;
+    }
+
+    /**
+     * Prepare input to be used in a SOLR query.
+     *
+     * Handles certain cases where the input might conflict with Lucene
+     * syntax rules.
+     *
+     * @param string $input Input string
+     *
+     * @return string
+     *
+     * @todo Check if it is safe to assume $input to be an UTF-8 encoded string.
+     */
+    protected function prepareForLuceneSyntax ($input)
+    {
+        // Normalize fancy quotes:
+        $quotes = array(
+            "\xC2\xAB"     => '"', // « (U+00AB) in UTF-8
+            "\xC2\xBB"     => '"', // » (U+00BB) in UTF-8
+            "\xE2\x80\x98" => "'", // ‘ (U+2018) in UTF-8
+            "\xE2\x80\x99" => "'", // ’ (U+2019) in UTF-8
+            "\xE2\x80\x9A" => "'", // ‚ (U+201A) in UTF-8
+            "\xE2\x80\x9B" => "'", // ? (U+201B) in UTF-8
+            "\xE2\x80\x9C" => '"', // “ (U+201C) in UTF-8
+            "\xE2\x80\x9D" => '"', // ” (U+201D) in UTF-8
+            "\xE2\x80\x9E" => '"', // „ (U+201E) in UTF-8
+            "\xE2\x80\x9F" => '"', // ? (U+201F) in UTF-8
+            "\xE2\x80\xB9" => "'", // ‹ (U+2039) in UTF-8
+            "\xE2\x80\xBA" => "'", // › (U+203A) in UTF-8
+        );
+        $input = strtr($input, $quotes);
+
+        // If the user has entered a lone BOOLEAN operator, convert it to lowercase
+        // so it is treated as a word (otherwise it will trigger a fatal error):
+        switch(trim($input)) {
+        case 'OR':
+            return 'or';
+        case 'AND':
+            return 'and';
+        case 'NOT':
+            return 'not';
+        }
+
+        // If the string consists only of control characters and/or BOOLEANs with no
+        // other input, wipe it out entirely to prevent weird errors:
+        $operators = array('AND', 'OR', 'NOT', '+', '-', '"', '&', '|');
+        if (trim(str_replace($operators, '', $input)) == '') {
+            return '';
+        }
+
+        // Translate "all records" search into a blank string
+        if (trim($input) == '*:*') {
+            return '';
+        }
+
+        // Ensure wildcards are not at beginning of input
+        if ((substr($input, 0, 1) == '*') || (substr($input, 0, 1) == '?')) {
+            $input = substr($input, 1);
+        }
+
+        // Ensure all parens match
+        //   Better: Remove all parens if they are not balanced
+        //     -- dmaus, 2012-11-11
+        $start = preg_match_all('/\(/', $input, $tmp);
+        $end = preg_match_all('/\)/', $input, $tmp);
+        if ($start != $end) {
+            $input = str_replace(array('(', ')'), '', $input);
+        }
+
+        // Ensure ^ is used properly
+        //   Better: Remove all ^ if not followed by digits
+        //     -- dmaus, 2012-11-11
+        $cnt = preg_match_all('/\^/', $input, $tmp);
+        $matches = preg_match_all('/.+\^[0-9]/', $input, $tmp);
+        if (($cnt) && ($cnt !== $matches)) {
+            $input = str_replace('^', '', $input);
+        }
+
+        // Remove unwanted brackets/braces that are not part of range queries.
+        // This is a bit of a shell game -- first we replace valid brackets and
+        // braces with tokens that cannot possibly already be in the query (due
+        // to ^ normalization in the step above).  Next, we remove all remaining
+        // invalid brackets/braces, and transform our tokens back into valid ones.
+        // Obviously, the order of the patterns/merges array is critically
+        // important to get this right!!
+        $patterns = array(
+            // STEP 1 -- escape valid brackets/braces
+            '/\[([^\[\]\s]+\s+TO\s+[^\[\]\s]+)\]/' .
+            ($this->caseSensitiveRanges ? '' : 'i'),
+            '/\{([^\{\}\s]+\s+TO\s+[^\{\}\s]+)\}/' .
+            ($this->caseSensitiveRanges ? '' : 'i'),
+            // STEP 2 -- destroy remaining brackets/braces
+            '/[\[\]\{\}]/',
+            // STEP 3 -- unescape valid brackets/braces
+            '/\^\^lbrack\^\^/', '/\^\^rbrack\^\^/',
+            '/\^\^lbrace\^\^/', '/\^\^rbrace\^\^/');
+        $matches = array(
+            // STEP 1 -- escape valid brackets/braces
+            '^^lbrack^^$1^^rbrack^^', '^^lbrace^^$1^^rbrace^^',
+            // STEP 2 -- destroy remaining brackets/braces
+            '',
+            // STEP 3 -- unescape valid brackets/braces
+            '[', ']', '{', '}');
+        $input = preg_replace($patterns, $matches, $input);
+        return $input;
+    }
+
+    /**
+     * Capitalize boolean operators.
+     *
+     * @param string $string Search string
+     *
+     * @return string
+     */
+    public function capitalizeBooleans ($string)
+    {
+        // Load the "inside quotes" lookahead so we can use it to prevent
+        // switching case of Boolean reserved words inside quotes, since
+        // that can cause problems in case-sensitive fields when the reserved
+        // words are actually used as search terms.
+        $lookahead = self::$insideQuotes;
+        $regs = array("/\s+AND\s+{$lookahead}/i", "/\s+OR\s+{$lookahead}/i",
+                "/(\s+NOT\s+|^NOT\s+){$lookahead}/i", "/\(NOT\s+{$lookahead}/i");
+        $replace = array(' AND ', ' OR ', ' NOT ', '(NOT ');
+        return trim(preg_replace($regs, $replace, $string));
+    }
+
+    /**
+     * Capitalize range operator.
+     *
+     * @param string $string Search string
+     *
+     * @return string
+     */
+    public function capitalizeRanges ($string)
+    {
+        // Load the "inside quotes" lookahead so we can use it to prevent
+        // switching case of ranges inside quotes, since that can cause
+        // problems in case-sensitive fields when the reserved words are
+        // actually used as search terms.
+        $lookahead = self::$insideQuotes;
+        $regs = array("/(\[)([^\]]+)\s+TO\s+([^\]]+)(\]){$lookahead}/i",
+            "/(\{)([^}]+)\s+TO\s+([^}]+)(\}){$lookahead}/i");
+        $callback = array($this, 'capitalizeRangesCallback');
+        return trim(preg_replace_callback($regs, $callback, $string));
+    }
+
+    /**
+     * Callback helper function.
+     *
+     * @param array $match Matches as of preg_replace_callback()
+     *
+     * @return string
+     *
+     * @see self::capitalizeRanges
+     *
+     * @todo Check possible problem with umlauts/non-ASCII word characters
+     */
+    protected function capitalizeRangesCallback ($match)
+    {
+        // Extract the relevant parts of the expression:
+        $open = $match[1];         // opening symbol
+        $close = $match[4];        // closing symbol
+        $start = $match[2];        // start of range
+        $end = $match[3];          // end of range
+
+        // Is this a case-sensitive range?
+        if (strtoupper($start) != strtolower($start)
+            || strtoupper($end) != strtolower($end)
+        ) {
+            // Build a lowercase version of the range:
+            $lower = $open . trim(strtolower($start)) . ' TO ' .
+                trim(strtolower($end)) . $close;
+            // Build a uppercase version of the range:
+            $upper = $open . trim(strtoupper($start)) . ' TO ' .
+                trim(strtoupper($end)) . $close;
+
+            // Special case: don't create illegal timestamps!
+            $timestamp = '/[0-9]{4}-[0-9]{2}-[0-9]{2}t[0-9]{2}:[0-9]{2}:[0-9]{2}z/i';
+            if (preg_match($timestamp, $start) || preg_match($timestamp, $end)) {
+                return $upper;
+            }
+
+            // Accept results matching either range:
+            return '(' . $lower . ' OR ' . $upper . ')';
+        } else {
+            // Simpler case -- case insensitive (probably numeric) range:
+            return $open . trim($start) . ' TO ' . trim($end) . $close;
+        }
+    }
+
+    /**
+     * Return from and to values of SOLR range.
+     *
+     * Returns false if the search string does not contain a range. Otherwise
+     * it returns an associative array with the keys `from', and `to'
+     * referring to the lower and upper bound of the range.
+     *
+     * @param string $string Search string
+     *
+     * @return array|false
+     *
+     * @todo This function seems to be unused
+     */
+    public function parseRange ($string)
+    {
+        $regEx = '/\[([^\]]+)\s+TO\s+([^\]]+)\]/';
+        if (!preg_match($regEx, $string, $matches)) {
+            return false;
+        }
+        return array('from' => trim($matches[1]), 'to' => trim($matches[2]));
+    }
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php
new file mode 100644
index 00000000000..7ccd3a89159
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * Simple, schema-less SOLR record.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr\Response\Json;
+
+use VuFindSearch\Response\RecordInterface;
+
+/**
+ * Simple, schema-less SOLR record.
+ *
+ * This record primarily servers as an example or blueprint for a schema-less
+ * record. All SOLR fields are exposed via object properties.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class Record implements RecordInterface
+{
+
+    /**
+     * SOLR fields.
+     *
+     * @var array
+     */
+    protected $fields;
+
+    /**
+     * Source identifier.
+     *
+     * @var string
+     */
+    protected $source;
+
+    /**
+     * Constructor.
+     *
+     * @param array $fields SOLR document fields
+     *
+     * @return void
+     */
+    public function __construct (array $fields)
+    {
+        $this->fields = $fields;
+    }
+
+    /**
+     * Set the source backend identifier.
+     *
+     * @param string $identifier Backend identifier
+     *
+     * @return void
+     */
+    public function setSourceIdentifier ($identifier)
+    {
+        $this->source = $identifier;
+    }
+
+    /**
+     * Return the source backend identifier.
+     *
+     * @return string
+     */
+    public function getSourceIdentifier ()
+    {
+        return $this->source;
+    }
+
+    /**
+     * __get()
+     *
+     * @param string $name Field name
+     *
+     * @return mixed
+     */
+    public function __get ($name)
+    {
+        return isset($this->fields[$name]) ? $this->fields[$name] : null;
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php
new file mode 100644
index 00000000000..ff59ad266ee
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php
@@ -0,0 +1,243 @@
+<?php
+
+/**
+ * Simple JSON-based record collection.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr\Response\Json;
+
+use VuFindSearch\Response\RecordCollectionInterface;
+use VuFindSearch\Response\RecordInterface;
+
+use VuFindSearch\Exception\RuntimeException;
+
+/**
+ * Simple JSON-based record collection.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class RecordCollection implements RecordCollectionInterface
+{
+
+    /**
+     * Deserialized SOLR response.
+     *
+     * @var array
+     */
+    protected $response;
+
+    /**
+     * Response records.
+     *
+     * @var array
+     */
+    protected $records;
+
+    /**
+     * Constructor.
+     *
+     * @param array $response Deserialized SOLR response
+     *
+     * @return void
+     */
+    public function __construct (array $response)
+    {
+        $this->response = $response;
+        $this->offset   = $response['response']['start'];
+        $this->records  = array();
+        $this->rewind();
+    }
+
+    /**
+     * Return total number of records found.
+     *
+     * @return int
+     */
+    public function getTotal ()
+    {
+        return $this->response['response']['numFound'];
+    }
+
+    /**
+     * Return query time in milli-seconds.
+     *
+     * @return float
+     */
+    public function getQueryTime ()
+    {
+        return $this->response['responseHeader']['QTime'];
+    }
+
+    /**
+     * Return available facets.
+     *
+     * Returns associative array of facets indexed by facet field. The value
+     * is a numeric array with available facets for this field. Each facet is
+     * a numeric array with the facet value followed by the facet count.
+     *
+     * @return array
+     */
+    public function getFacets ()
+    {
+        return $this->response['response']['facet_fields'];
+    }
+
+    /**
+     * Return records.
+     *
+     * @return array
+     */
+    public function getRecords ()
+    {
+        return $this->records;
+    }
+
+    /**
+     * Return offset in the total search result set.
+     *
+     * @return int
+     */
+    public function getOffset ()
+    {
+        return $this->response['response']['start'];
+    }
+
+    /**
+     * Return first record in response.
+     *
+     * @return RecordInterface|null
+     */
+    public function first ()
+    {
+        return isset($this->records[$this->offset]) ? $this->records[$this->offset] : null;
+    }
+
+    /**
+     * Set the source backend identifier.
+     *
+     * @param string $identifier Backend identifier
+     *
+     * @return void
+     */
+    public function setSourceIdentifier ($identifier)
+    {
+        $this->source = $identifier;
+    }
+
+    /**
+     * Return the source backend identifier.
+     *
+     * @return string
+     */
+    public function getSourceIdentifier ()
+    {
+        return $this->source;
+    }
+
+    /**
+     * Add a record to the collection.
+     *
+     * @param RecordInterface $record Record to add
+     *
+     * @return void
+     */
+    public function add (RecordInterface $record)
+    {
+        if (!in_array($record, $this->records, true)) {
+            $this->records[$this->pointer] = $record;
+            $this->next();
+        }
+    }
+
+    /// Iterator interface
+
+    /**
+     * Return true if current collection index is valid.
+     *
+     * @return boolean
+     */
+    public function valid ()
+    {
+        return isset($this->records[$this->pointer]);
+    }
+
+    /**
+     * Return record at current collection index.
+     *
+     * @return RecordInterface
+     */
+    public function current ()
+    {
+        return $this->records[$this->pointer];
+    }
+
+    /**
+     * Rewind collection index.
+     *
+     * @return void
+     */
+    public function rewind ()
+    {
+        $this->pointer = $this->offset;
+    }
+
+    /**
+     * Move to next collection index.
+     *
+     * @return void
+     */
+    public function next ()
+    {
+        $this->pointer++;
+    }
+
+    /**
+     * Return current collection index.
+     *
+     * @return integer
+     */
+    public function key ()
+    {
+        return $this->pointer;
+    }
+
+    /// Countable interface
+
+    /**
+     * Return number of records in collection.
+     *
+     * @return integer
+     */
+    public function count ()
+    {
+        return count($this->records);
+    }
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php
new file mode 100644
index 00000000000..118dd71d42d
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Simple JSON-based factory for record collection.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr\Response\Json;
+
+use VuFindSearch\Response\RecordCollectionFactoryInterface;
+use VuFindSearch\Exception\InvalidArgumentException;
+
+/**
+ * Simple JSON-based factory for record collection.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class RecordCollectionFactory implements RecordCollectionFactoryInterface
+{
+    /**
+     * Class of collection records.
+     *
+     * @var string
+     */
+    protected $recordClass;
+
+    /**
+     * Class of collection.
+     *
+     * @var string
+     */
+    protected $collectionClass;
+
+    /**
+     * Constructor.
+     *
+     * @param string $recordClass     Class of collection records
+     * @param string $collectionClass Class of collection
+     *
+     * @return void
+     */
+    public function __construct ($recordClass = 'VuFindSearch\Backend\Solr\Response\Json\Record', $collectionClass = 'VuFindSearch\Backend\Solr\Response\Json\RecordCollection')
+    {
+        $this->recordClass     = $recordClass;
+        $this->collectionClass = $collectionClass;
+    }
+
+    /**
+     * Return record collection.
+     *
+     * @param array $response Deserialized JSON response
+     *
+     * @return RecordCollection
+     */
+    public function factory ($response)
+    {
+        if (!is_array($response)) {
+            throw new InvalidArgumentException(sprintf('Unexpected type of value: Expected array, got %s', gettype($response)));
+        }
+        $collection = new $this->collectionClass($response);
+        foreach ($response['response']['docs'] as $doc) {
+            $collection->add(new $this->recordClass($doc));
+        }
+        return $collection;
+    }
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php
new file mode 100644
index 00000000000..500ca525b93
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php
@@ -0,0 +1,448 @@
+<?php
+
+/**
+ * VuFind SearchHandler.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Backend\Solr;
+
+/**
+ * VuFind SearchHandler.
+ *
+ * The SearchHandler implements the rule-based translation of a user search
+ * query to a SOLR query string.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class SearchHandler
+{
+
+    /**
+     * Known configuration keys.
+     *
+     * @var array
+     */
+    protected static $configKeys = array(
+        'CustomMunge', 'DismaxFields', 'QueryFields', 'DismaxParams', 'FilterQuery'
+    );
+
+    /**
+     * Known boolean operators.
+     *
+     * @var array
+     */
+    protected static $booleanOperators = array('AND', 'OR', 'NOT');
+
+    /**
+     * Search handler specification.
+     *
+     * @var array
+     */
+    protected $specs;
+
+    /**
+     * Constructor.
+     *
+     * @param array $spec Search handler specification
+     *
+     * @return void
+     */
+    public function __construct (array $spec)
+    {
+        foreach (self::$configKeys as $key) {
+            $this->specs[$key] = isset($spec[$key]) ? $spec[$key] : array();
+        }
+    }
+
+    /// Public API
+
+    /**
+     * Return an advanced query string.
+     *
+     * An advanced query string is a query string based on a search string w/
+     * lucene syntax features.
+     *
+     * @param string $search Search string
+     *
+     * @return string
+     *
+     * @see \VuFind\Service\Solr\QueryBuilder::containsAdvancedLuceneSyntax()
+     */
+    public function createAdvancedQueryString ($search)
+    {
+        return $this->createQueryString($search, true);
+    }
+
+    /**
+     * Return a simple query string.
+     *
+     * @param string $search Search string
+     *
+     * @return string
+     *
+     * @see \VuFind\Service\Solr\SearchHandler::createAdvancedQueryString()
+     */
+    public function createSimpleQueryString ($search)
+    {
+        return $this->createQueryString($search, false);
+    }
+
+    /**
+     * Return an advanced query string for specified search string.
+     *
+     * @param string $search Search string
+     *
+     * @return string
+     */
+    public function createBoostQueryString ($search)
+    {
+        $boostQuery = array();
+        if ($this->hasDismax()) {
+            foreach ($this->getDismaxParams() as $param) {
+                list($name, $value) = $param;
+                if ($name === 'bq') {
+                    $boostQuery[] = $value;
+                } else if ($name === 'bf') {
+                    // BF parameter may contain multiple space-separated functions
+                    // with individual boosts.  We need to parse this into _val_
+                    // query components:
+                    foreach (explode(' ', $value) as $boostFunction) {
+                        if ($boostFunction) {
+                            $parts = explode('^', $boostFunction, 2);
+                            $boostQuery[] = sprintf(
+                                '_val_:"%s"%s',
+                                addcslashes($parts[0], '"'),
+                                isset($parts[1]) ? "^{$parts[1]}" : ''
+                            );
+                        }
+                    }
+                }
+            }
+        }
+        if ($boostQuery) {
+            return sprintf(
+                '(%s) AND (*:* OR %s)',
+                $search, implode(' OR ', $boostQuery)
+            );
+        } else {
+            return $search;
+        }
+    }
+
+    /**
+     * Return true if the handler defines Dismax fields.
+     *
+     * @return boolean
+     */
+    public function hasDismax ()
+    {
+        return !empty($this->specs['DismaxFields']);
+    }
+
+    /**
+     * Return defined dismax fields.
+     *
+     * @return array
+     */
+    public function getDismaxFields ()
+    {
+        return $this->specs['DismaxFields'];
+    }
+
+    /**
+     * Return defined dismax parameters.
+     *
+     * @return array
+     */
+    public function getDismaxParams ()
+    {
+        return $this->specs['DismaxParams'];
+    }
+
+    /**
+     * Return the filter query.
+     *
+     * @return string
+     *
+     */
+    public function getFilterQuery ()
+    {
+        return empty($this->specs['FilterQuery']) ? null : $this->specs['FilterQuery'];
+    }
+
+    /**
+     * Return true if handler defines a filter query.
+     *
+     * @return boolean
+     */
+    public function hasFilterQuery ()
+    {
+        return (bool)$this->specs['FilterQuery'];
+    }
+
+    /**
+     * Serialize handler specs as array.
+     *
+     * @return array
+     */
+    public function toArray ()
+    {
+        return $this->specs;
+    }
+
+    /// Internal API
+
+    /**
+     * Return a Dismax subquery for specified search string.
+     *
+     * @param string $search Search string
+     *
+     * @return string
+     */
+    protected function dismaxSubquery ($search)
+    {
+        $dismaxParams = array();
+        foreach ($this->specs['DismaxParams'] as $param) {
+            $dismaxParams[] = sprintf(
+                "%s='%s'", $param[0], addcslashes($param[1], "'")
+            );
+        }
+        $dismaxQuery = sprintf(
+            '{!dismax qf="%s" %s}%s',
+            implode(' ', $this->specs['DismaxFields']),
+            implode(' ', $dismaxParams),
+            $search
+        );
+        return sprintf('_query_:"%s"', addslashes($dismaxQuery));
+    }
+
+    /**
+     * Return the munge values for specified search string.
+     *
+     * If optional argument $tokenize is true tokenize the search string.
+     *
+     * @param string  $search   Search string
+     * @param boolean $tokenize Tokenize the search string?
+     *
+     * @return string
+     */
+    protected function mungeValues ($search, $tokenize = true)
+    {
+        if ($tokenize) {
+            $tokens = $this->tokenize($search);
+            $mungeValues = array(
+                'onephrase' => sprintf(
+                    '"%s"', str_replace('"', '', implode(' ', $tokens))
+                ),
+                'and' => implode(' AND ', $tokens),
+                'or'  => implode(' OR ', $tokens),
+            );
+        } else {
+            $mungeValues = array(
+                'and' => $search,
+                'or'  => $search,
+            );
+            // If we're skipping tokenization, we just want to pass $lookfor through
+            // unmodified (it's probably an advanced search that won't benefit from
+            // tokenization).  We'll just set all possible values to the same thing,
+            // except that we'll try to do the "one phrase" in quotes if possible.
+            // IMPORTANT: If we detect a boolean NOT, we MUST omit the quotes.
+            if (strstr($search, '"') || strstr($search, ' NOT ')) {
+                $mungeValues['onephrase'] = $search;
+            } else {
+                $mungeValues['onephrase'] = sprintf('"%s"', $search);
+            }
+        }
+
+        foreach ($this->specs['CustomMunge'] as $mungeName => $mungeOps) {
+            $mungeValues[$mungeName] = $search;
+            foreach ($mungeOps as $operation) {
+                switch ($operation[0]) {
+                case 'append':
+                    $mungeValues[$mungeName] .= $operation[1];
+                    break;
+                case 'lowercase':
+                    $mungeValues[$mungeName] = strtolower($mungeValues[$mungeName]);
+                    break;
+                case 'preg_replace':
+                    $mungeValues[$mungeName] = preg_replace(
+                        $operation[1], $operation[2], $mungeValues[$mungeName]
+                    );
+                    break;
+                case 'uppercase':
+                    $mungeValues[$mungeName] = strtoupper($mungeValues[$mungeName]);
+                    break;
+                default:
+                    throw new \InvalidArgumentException(
+                        sprintf('Unknown munge operation: %s', $operation[0])
+                    );
+                }
+            }
+        }
+        return $mungeValues;
+    }
+
+    /**
+     * Return query string for specified search string.
+     *
+     * If optional argument $advanced is true the search string contains
+     * advanced lucene query syntax.
+     *
+     * @param string  $search   Search string
+     * @param boolean $advanced Is the search an advanced search string?
+     *
+     * @return string
+     *
+     */
+    protected function createQueryString ($search, $advanced = false)
+    {
+
+        // If this is a basic query and we have Dismax settings, let's build
+        // a Dismax subquery to avoid some of the ugly side effects of our Lucene
+        // query generation logic.
+        if (!$advanced && $this->hasDismax()) {
+            $query = $this->dismaxSubquery($search);
+        } else {
+            $mungeRules  = $this->mungeRules();
+            // Do not munge w/o rules
+            if ($mungeRules) {
+                $mungeValues = $this->mungeValues($search, !$advanced);
+                $query       = $this->munge($mungeRules, $mungeValues);
+            } else {
+                $query = $search;
+            }
+        }
+        if ($this->hasFilterQuery()) {
+            $query = sprintf('(%s) AND (%s)', $query, $this->getFilterQuery());
+        }
+        return "($query)";
+    }
+
+    /**
+     * Return array of munge rules.
+     *
+     * @todo Maybe rename?
+     *
+     * @return array
+     */
+    protected function mungeRules ()
+    {
+        return $this->specs['QueryFields'];
+    }
+
+    /**
+     * Return modified search strign after applying the transformation rules.
+     *
+     * @param array  $mungeRules  Munge rules
+     * @param array  $mungeValues Munge values
+     * @param string $joiner      Joiner of subqueries
+     *
+     * @return string
+     */
+    protected function munge (array $mungeRules, array $mungeValues, $joiner = 'OR')
+    {
+        $clauses = array();
+        foreach ($mungeRules as $field => $clausearray) {
+            if (is_numeric($field)) {
+                // shift off the join string and weight
+                $sw = array_shift($clausearray);
+                $internalJoin = ' ' . $sw[0] . ' ';
+                // Build it up recursively
+                $sstring = '(' .
+                    $this->munge($clausearray, $mungeValues, $internalJoin) .
+                    ')';
+                // ...and add a weight if we have one
+                $weight = $sw[1];
+                if (!is_null($weight) && $weight && $weight > 0) {
+                    $sstring .= '^' . $weight;
+                }
+                // push it onto the stack of clauses
+                $clauses[] = $sstring;
+            } else {
+                // Otherwise, we've got a (list of) [munge, weight] pairs to deal
+                // with
+                foreach ($clausearray as $spec) {
+                    // build a string like title:("one two")
+                    $sstring = $field . ':(' . $mungeValues[$spec[0]] . ')';
+                    // Add the weight if we have one. Yes, I know, it's redundant
+                    // code.
+                    $weight = $spec[1];
+                    if (!is_null($weight) && $weight && $weight > 0) {
+                        $sstring .= '^' . $weight;
+                    }
+                    // ..and push it on the stack of clauses
+                    $clauses[] = $sstring;
+                }
+            }
+        }
+
+        // Join it all together
+        return implode(' ' . $joiner . ' ', $clauses);
+    }
+
+    /**
+     * Tokenize the search string.
+     *
+     * @param string $string Search string
+     *
+     * @return array
+     */
+    protected function tokenize ($string)
+    {
+        // Tokenize on spaces and quotes
+        $phrases = array();
+        preg_match_all('/"[^"]*"[~[0-9]+]*|"[^"]*"|[^ ]+/', $string, $phrases);
+        $phrases = $phrases[0];
+
+        $tokens  = array();
+        $token   = array();
+
+        reset($phrases);
+        while (current($phrases) !== false) {
+            $token[] = current($phrases);
+            $next    = next($phrases);
+            if (in_array($next, self::$booleanOperators)) {
+                $token[] = $next;
+                if (next($phrases) === false) {
+                    $tokens[] = implode(' ', $token);
+                }
+            } else {
+                $tokens[] = implode(' ', $token);
+                $token = array();
+            }
+        }
+
+        return $tokens;
+    }
+}
diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php b/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php
new file mode 100644
index 00000000000..778eccce2be
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Marker interface.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+
+namespace VuFindSearch\Exception;
+
+/**
+ * Marker interface.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+interface ExceptionInterface
+{
+}
diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php b/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php
new file mode 100644
index 00000000000..ebe0b00ffb0
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * Invalid argument exception.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Exception;
+
+/**
+ * Invalid argument exception.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php b/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php
new file mode 100644
index 00000000000..dfd51ba60d6
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Generic RuntimeException.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+
+namespace VuFindSearch\Exception;
+
+/**
+ * Generic RuntimeException.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/ParamBag.php b/module/VuFindSearch/src/VuFindSearch/ParamBag.php
new file mode 100644
index 00000000000..56d529b7b81
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/ParamBag.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * Parameter bag.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch;
+
+/**
+ * Lightweight wrapper for request parameters.
+ *
+ * This class represents the request parameters. Parameters are stored in an
+ * associative array with the parameter name as key. Because e.g. SOLR allows
+ * repeated query parameters the values are always stored in an array.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class ParamBag
+{
+
+    /**
+     * Parameters
+     *
+     * @var array
+     */
+    protected $params = array();
+
+    /**
+     * Constructor.
+     *
+     * @param array $initial Initial parameters
+     *
+     * @return void
+     */
+    public function __construct (array $initial = array())
+    {
+        foreach ($initial as $name => $value) {
+            $this->add($name, $value);
+        }
+    }
+
+    /**
+     * Return parameter value.
+     *
+     * @param string $name Parameter name
+     *
+     * @return mixed|null Parameter value or NULL if not set
+     */
+    public function get ($name)
+    {
+        return isset($this->params[$name]) ? $this->params[$name] : null;
+    }
+
+    /**
+     * Set a parameter.
+     *
+     * @param string $name  Parameter name
+     * @param string $value Parameter value
+     *
+     * @return void
+     */
+    public function set ($name, $value)
+    {
+        if (is_array($value)) {
+            $this->params[$name] = $value;
+        } else {
+            $this->params[$name] = array($value);
+        }
+    }
+
+    /**
+     * Remove a parameter.
+     *
+     * @param string $name Parameter name
+     *
+     * @return void
+     */
+    public function remove ($name)
+    {
+        if (isset($this->params[$name])) {
+            unset($this->params[$name]);
+        }
+    }
+
+    /**
+     * Add parameter value.
+     *
+     * @param string $name  Parameter name
+     * @param mixed  $value Parameter value
+     *
+     * @return void
+     */
+    public function add ($name, $value)
+    {
+        if (!isset($this->params[$name])) {
+            $this->params[$name] = array();
+        }
+        if (is_array($value)) {
+            $this->params[$name] = array_merge($this->params[$name], $value);
+        } else {
+            $this->params[$name][] = $value;
+        }
+    }
+
+    /**
+     * Return array of request parameters.
+     *
+     * @return array
+     */
+    public function params ()
+    {
+        return $this->params;
+    }
+
+    /**
+     * Merge with another parameter bag.
+     *
+     * @param ParamBag $bag Parameter bag to merge with
+     *
+     * @return void
+     */
+    public function mergeWith (ParamBag $bag)
+    {
+        foreach ($bag->params as $key => $value) {
+            if (!empty($value)) {
+                $this->add($key, $value);
+            }
+        }
+    }
+
+    /**
+     * Merge with all supplied parameter bags.
+     *
+     * @param array $bags Parameter bags to merge with
+     *
+     * @return void
+     */
+    public function mergeWithAll (array $bags)
+    {
+        foreach ($bags as $bag) {
+            $this->mergeWith($bag);
+        }
+    }
+
+    /**
+     * Return array of params ready to be used in a HTTP request.
+     *
+     * Returns a numerical array with all request parameters as properly URL
+     * encoded key-value pairs.
+     *
+     * @return array
+     */
+    public function request ()
+    {
+        $request = array();
+        foreach ($this->params as $name => $values) {
+            if (!empty($values)) {
+                $request = array_merge(
+                    $request,
+                    array_map(
+                        function ($value) use ($name) {
+                            return sprintf('%s=%s', urlencode($name), urlencode($value));
+                        },
+                        $values
+                    )
+                );
+            }
+        }
+        return $request;
+    }
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php b/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php
new file mode 100644
index 00000000000..620a9649c61
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * Abstract base class of user query components.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Query;
+
+/**
+ * Abstract base class of user query components.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+abstract class AbstractQuery
+{}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/Params.php b/module/VuFindSearch/src/VuFindSearch/Query/Params.php
new file mode 100644
index 00000000000..314cf92b375
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Query/Params.php
@@ -0,0 +1,269 @@
+<?php
+
+/**
+ * Query parameter class file.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category Search
+ * @package  Query
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/system_classes Wiki
+ */
+
+
+namespace VuFindSearch\Query;
+
+/**
+ * Query parameter class.
+ *
+ * @category Search
+ * @package  Query
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/system_classes Wiki
+ */
+
+class Params
+{
+
+    /**
+     * Sort order.
+     *
+     * @var string
+     */
+    protected $sort = 'relevance';
+
+    /**
+     * Maximum number of records per result.
+     *
+     * @var int
+     */
+    protected $limit = 10;
+
+    /**
+     * Offset in entire result set.
+     *
+     * @var int
+     */
+    protected $offset = 0;
+
+    /**
+     * Facet settings.
+     *
+     * @var array
+     */
+    protected $facets = array();
+
+    /**
+     * Filter settings.
+     *
+     * @var array
+     */
+    protected $filters = array();
+
+    /**
+     * Spellcheck dictionary.
+     *
+     * @var string
+     */
+    protected $spellcheckDictionary;
+
+    /**
+     * Is spellcheck enabled?
+     *
+     * @var boolean
+     */
+    protected $spellcheckEnabled;
+
+    /**
+     * Return facet settings.
+     *
+     * @return array
+     */
+    public function getFacets ()
+    {
+        return $this->facets;
+    }
+
+    /**
+     * Set facet settings.
+     *
+     * @param array $facets Facet settings
+     *
+     * @return void
+     */
+    public function setFacets (array $facets)
+    {
+        $this->facets = $facets;
+    }
+
+    /**
+     * Return filter settings.
+     *
+     * @return array
+     */
+    public function getFilters ()
+    {
+        return $this->filters;
+    }
+
+    /**
+     * Set filter settings.
+     *
+     * @param string $field Filter field
+     * @param string $value Filter value
+     *
+     * @return void
+     */
+    public function setFilter ($field, $value)
+    {
+        $this->filters[$field] = $value;
+    }
+
+    /**
+     * Return selected shards.
+     *
+     * @return array
+     */
+    public function getShards ()
+    {
+        return array();
+    }
+
+    /**
+     * Return spellcheck dictionary.
+     *
+     * @return array
+     */
+    public function getSpellcheckDictionary ()
+    {
+        return $this->spellcheckDictionary;
+    }
+
+    /**
+     * Set spellcheck dictionary.
+     *
+     * @param  string $dictionary
+     * @return void
+     */
+    public function setSpellcheckDictionary ($dictionary)
+    {
+        $this->spellcheckDictionary = $dictionary;
+    }
+
+    /**
+     * Return true if spellcheck suggestions are enabled.
+     *
+     * @return boolean
+     */
+    public function isSpellcheckEnabled ()
+    {
+        return $this->spellcheckEnabled;
+    }
+
+    /**
+     * Enable or disable spellcheck.
+     *
+     * @param  boolean $enable
+     * @return void
+     */
+    public function enableSpellcheck ($enable)
+    {
+        $this->spellcheckEnabled = (boolean)$enable;
+    }
+
+    /**
+     * Return highlighting settings.
+     *
+     * Dummy implementation
+     *
+     * @return array
+     */
+    public function getHighlighting ()
+    {
+        return array();
+    }
+
+    /**
+     * Return sort order.
+     *
+     * @return string
+     */
+    public function getSort ()
+    {
+        return $this->sort;
+    }
+
+    /**
+     * Set sort order.
+     *
+     * @param string $order
+     *
+     * @return void
+     */
+    public function setSort ($sort)
+    {
+        $this->sort = (string)$sort;
+    }
+
+    /**
+     * Return limit.
+     *
+     * @return int
+     */
+    public function getLimit ()
+    {
+        return $this->limit;
+    }
+
+    /**
+     * Set limit.
+     *
+     * @param int $limit
+     *
+     * @return void
+     */
+    public function setLimit ($limit)
+    {
+        $this->limit = (int)$limit;
+    }
+
+    /**
+     * Return offset.
+     *
+     * @return int
+     */
+    public function getOffset ()
+    {
+        return $this->offset;
+    }
+
+    /**
+     * Set offset.
+     *
+     * @param int $offset
+     *
+     * @return void
+     */
+    public function setOffset ($offset)
+    {
+        $this->offset = (int)$offset;
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/Query.php b/module/VuFindSearch/src/VuFindSearch/Query/Query.php
new file mode 100644
index 00000000000..41e5bace2da
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Query/Query.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * A single/simple query.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Query;
+
+/**
+ * A single/simple query.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class Query extends AbstractQuery
+{
+
+    /**
+     * Name of query handler, if any.
+     *
+     * @var string
+     */
+    protected $queryHandler;
+
+    /**
+     * Query string
+     *
+     * @var string
+     */
+    protected $queryString;
+
+    /**
+     * Constructor.
+     *
+     * @param string $string  Search string
+     * @param string $handler Name of search handler
+     *
+     * @return void
+     */
+    public function __construct ($string = null, $handler = null)
+    {
+        $this->queryHandler = $handler ? strtolower($handler) : null;
+        $this->queryString  = $string;
+    }
+
+    /**
+     * Return search string.
+     *
+     * @return string
+     */
+    public function getString ()
+    {
+        return $this->queryString;
+    }
+
+    /**
+     * Set the search string.
+     *
+     * @param string $string New search string
+     *
+     * @return void
+     */
+    public function setString ($string)
+    {
+        $this->queryString = $string;
+    }
+
+    /**
+     * Return name of search handler.
+     *
+     * @return string
+     */
+    public function getHandler ()
+    {
+        return $this->queryHandler;
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php b/module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php
new file mode 100644
index 00000000000..7a30d86328c
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Query/QueryAdapter.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * QueryAdapter class file.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category Search
+ * @package  Query
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/system_classes Wiki
+ */
+
+namespace VuFindSearch\Query;
+
+/**
+ * Adapter for legacy query representation.
+ *
+ * @category Search
+ * @package  Query
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/system_classes Wiki
+ */
+
+abstract class QueryAdapter
+{
+
+    /**
+     * Return a Query or QueryGroup based on user search arguments.
+     *
+     * @param array $search Search arguments
+     *
+     * @return \VuFind\Search\Query|\VuFind\Search\QueryGroup
+     */
+    public static function create (array $search)
+    {
+        if (isset($search['lookfor'])) {
+            $handler = isset($search['index']) ? $search['index'] : $search['field'];
+            return new Query($search['lookfor'], $handler);
+        } elseif (isset($search['group'])) {
+            $operator = $search['group'][0]['bool'];
+            return new QueryGroup($operator, array_map(array('self', 'create'), $search['group']));
+        } else {
+            // Special case: The outer-most group-of-groups.
+            if (isset($search[0]['join'])) {
+                $operator = $search[0]['join'];
+                return new QueryGroup($operator, array_map(array('self', 'create'), $search));
+            } else {
+                // Simple query
+                return new Query($search[0]['lookfor'], $search[0]['index']);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php b/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php
new file mode 100644
index 00000000000..7403c0f3e84
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php
@@ -0,0 +1,210 @@
+<?php
+
+/**
+ * A group of single/simples queries, joined by boolean operator.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch\Query;
+
+use VuFindSearch\Exception\InvalidArgumentException;
+
+/**
+ * A group of single/simples queries, joined by boolean operator.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class QueryGroup extends AbstractQuery
+{
+
+    /**
+     * Valid boolean operators.
+     *
+     * @var array
+     */
+    protected static $operators = array('AND', 'OR', 'NOT');
+
+    /**
+     * Name of the handler to be used if the query group is reduced.
+     *
+     * @see VuFindSearch\Backend\Solr\QueryBuilder::reduceQueryGroup()
+     *
+     * @var string
+     *
+     * @todo Check if we actually use/need this feature
+     */
+    protected $reducedHandler;
+
+    /**
+     * Boolean operator.
+     *
+     * @var string
+     */
+    protected $operator;
+
+    /**
+     * Is the query group negated?
+     *
+     * @var boolean
+     */
+    protected $negation;
+
+    /**
+     * Queries.
+     *
+     * @var array
+     */
+    protected $queries;
+
+    /**
+     * Constructor.
+     *
+     * @param string $operator       Boolean operator
+     * @param array  $queries        Queries
+     * @param string $reducedHandler Handler to be uses if reduced
+     *
+     * @return void
+     */
+    public function __construct ($operator, array $queries = array(), $reducedHandler = null)
+    {
+        $this->setOperator($operator);
+        $this->setQueries($queries);
+        $this->setReducedHandler($reducedHandler);
+    }
+
+    /**
+     * Return name of reduced handler.
+     *
+     * @return string|null
+     */
+    public function getReducedHandler ()
+    {
+        return $this->reducedHandler;
+    }
+
+    /**
+     * Set name of reduced handler.
+     *
+     * @param string $handler Reduced handler
+     *
+     * @return void
+     */
+    public function setReducedHandler ($handler)
+    {
+        $this->reducedHandler = $handler;
+    }
+
+    /**
+     * Unset reduced handler.
+     *
+     * @return void
+     */
+    public function unsetReducedHandler ()
+    {
+        $this->reducedHandler = null;
+    }
+
+    /**
+     * Add a query to the group.
+     *
+     * @param \VuFind\Search\AbstractQuery $query Query to add
+     *
+     * @return void
+     */
+    public function addQuery (AbstractQuery $query)
+    {
+        $this->queries[] = $query;
+    }
+
+    /**
+     * Return group queries.
+     *
+     * @return array
+     */
+    public function getQueries ()
+    {
+        return $this->queries;
+    }
+
+    /**
+     * Set group queries.
+     *
+     * @param array $queries Group queries
+     *
+     * @return void
+     */
+    public function setQueries (array $queries)
+    {
+        foreach ($queries as $query) {
+            $this->addQuery($query);
+        }
+    }
+
+    /**
+     * Set boolean operator.
+     *
+     * @param string $operator Boolean operator
+     *
+     * @return void
+     *
+     * @throws \InvalidArgumentException Unknown or invalid boolean operator
+     */
+    public function setOperator ($operator)
+    {
+        if (!in_array($operator, self::$operators)) {
+            throw new InvalidArgumentException("Unknown or invalid boolean operator: {$operator}");
+        }
+        if ($operator == 'NOT') {
+            $this->operator = 'OR';
+            $this->negation = true;
+        } else {
+            $this->operator = $operator;
+        }
+    }
+
+    /**
+     * Return boolean operator.
+     *
+     * @return string
+     */
+    public function getOperator ()
+    {
+        return $this->operator;
+    }
+
+    /**
+     * Return true if group is an exclusion group.
+     *
+     * @return boolean
+     */
+    public function isNegated ()
+    {
+        return $this->negation;
+    }
+}
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php
new file mode 100644
index 00000000000..ac874cee074
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace VuFindSearch\Response;
+
+interface RecordCollectionFactoryInterface
+{
+    public function factory ($response);
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php
new file mode 100644
index 00000000000..0ec8069b0fc
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * Search backend search response interface file.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category Search
+ * @package  Service
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+
+namespace VuFindSearch\Response;
+
+/**
+ * Interface for backend responses to a search() operation.
+ *
+ * @category Search
+ * @package  Service
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+interface RecordCollectionInterface extends \Countable, \Iterator
+{
+
+    /**
+     * Return total number of records found.
+     *
+     * @return int
+     */
+    public function getTotal ();
+
+    /**
+     * Return query time in milli-seconds.
+     *
+     * @return float
+     */
+    public function getQueryTime ();
+
+    /**
+     * Return available facets.
+     *
+     * Returns an associative array with the internal field name as key. The
+     * value is an associative array of the available facets for the field,
+     * indexed by facet value.
+     *
+     * @return array
+     */
+    public function getFacets ();
+
+    /**
+     * Return records.
+     *
+     * @return array
+     */
+    public function getRecords ();
+
+    /**
+     * Return offset in the total search result set.
+     *
+     * @return int
+     */
+    public function getOffset ();
+
+    /**
+     * Return first record in collection.
+     *
+     * @return RecordInterface|null
+     */
+    public function first ();
+
+    /**
+     * Set the source backend identifier.
+     *
+     * @param string $identifier Backend identifier
+     *
+     * @return void
+     */
+    public function setSourceIdentifier ($identifier);
+
+    /**
+     * Return the source backend identifier.
+     *
+     * @return string
+     */
+    public function getSourceIdentifier ();
+
+    /**
+     * Add a record to the collection.
+     *
+     * @param RecordInterface $record
+     *
+     * @return void
+     */
+    public function add (RecordInterface $record);
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php
new file mode 100644
index 00000000000..305c8a3f9b0
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Record interface file.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category Search
+ * @package  Service
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+
+namespace VuFindSearch\Response;
+
+/**
+ * Record interface.
+ *
+ * Every record must implement this.
+ *
+ * @category Search
+ * @package  Service
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://github.com/dmj/vf2-search-subsystem
+ */
+interface RecordInterface
+{
+
+    /**
+     * Set the source backend identifier.
+     *
+     * @param string $identifier Backend identifier
+     *
+     * @return void
+     */
+    public function setSourceIdentifier ($identifier);
+
+    /**
+     * Return the source backend identifier.
+     *
+     * @return string
+     */
+    public function getSourceIdentifier ();
+
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/src/VuFindSearch/Service.php b/module/VuFindSearch/src/VuFindSearch/Service.php
new file mode 100644
index 00000000000..533aea4c5ff
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Service.php
@@ -0,0 +1,296 @@
+<?php
+
+/**
+ * Search service.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindSearch;
+
+use VuFindSearch\Backend\BackendInterface;
+
+use Zend\Log\LoggerInterface;
+use Zend\EventManager\EventManagerInterface;
+use Zend\EventManager\EventManager;
+
+/**
+ * Search service.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class Service
+{
+
+    /**
+     * Event manager.
+     *
+     * @var EventManager
+     */
+    protected $events;
+
+    /**
+     * Logger, if any.
+     *
+     * @var LoggerInterface
+     */
+    protected $logger;
+
+    /**
+     * Cache resolved backends.
+     *
+     * @var array
+     */
+    protected $backends;
+
+    /**
+     * Constructor.
+     *
+     * @return void
+     */
+    public function __construct ()
+    {
+        $this->backends = array();
+    }
+
+    /**
+     * Perform a search and return a wrapped response.
+     *
+     * @param string        $backend Search backend identifier
+     * @param AbstractQuery $query   Search query
+     * @param Params        $params  Search parameters
+     *
+     * @return ResponseInterface
+     *
+     */
+    public function search ($backend, Query\AbstractQuery $query, Query\Params $params)
+    {
+        $context = __FUNCTION__;
+        $args = compact('backend', 'query', 'params', 'context');
+        $backend  = $this->resolve($backend, $args);
+
+        $this->triggerPre($context, $backend, $args);
+        $response = $backend->search($query, $params);
+        $this->triggerPost($context, $response, $args);
+        return $response;
+    }
+
+    /**
+     * Retrieve a single record.
+     *
+     * @param string $backend Search backend identifier
+     * @param string $id      Record identifier
+     *
+     * @return ResponseInterface
+     */
+    public function retrieve ($backend, $id)
+    {
+        $context = __FUNCTION__;
+        $args = compact('backend', 'id', 'context');
+        $backend = $this->resolve($backend, $args);
+
+        $this->triggerPre($context, $backend, $args);
+        $response = $backend->retrieve($id);
+        $this->triggerPost($context, $response, $args);
+        return $response;
+    }
+
+    /**
+     * Delete a single record.
+     *
+     * @param string $backend Search backend identifier
+     * @param string $id      Record identifier
+     *
+     * @return null
+     */
+    public function delete ($backend, $id)
+    {
+        $context = __FUNCTION__;
+        $args = compact('backend', 'id', 'context');
+        $backend = $this->resolve($backend, $args);
+
+        $this->triggerPre($context, $backend, $args);
+        $response = $backend->delete($id);
+        $this->triggerPost($context, $response, $args);
+    }
+
+    /**
+     * Delete all records.
+     *
+     * @param string $backend Search backend identifier
+     *
+     * @return null
+     */
+    public function deleteAll ($backend)
+    {
+        $context = __FUNCTION__;
+        $args = compact('backend', 'context');
+        $backend = $this->resolve($backend, $args);
+
+        $this->triggerPre($context, $backend, $args);
+        $response = $backend->deleteAll();
+        $this->triggerPost($context, $response, $args);
+        return $response;
+    }
+
+    /**
+     * Update a record.
+     *
+     * @param string          $backend Search backend identifier
+     * @param RecordInterface $record  The record to update
+     *
+     * @return void
+     */
+    public function update ($backend, RecordInterface $record)
+    {
+        $context = __FUNCTION__;
+        $args = compact('backend', 'record', 'context');
+        $backend = $this->resolve($backend, $args);
+
+        $this->triggerPre($context, $backend, $args);
+        $response = $backend->update($record);
+        $this->triggerPost($context, $response, $args);
+    }
+
+    /**
+     * Set application logger.
+     *
+     * @param LoggerInterface $logger Logger
+     *
+     * @return void
+     */
+    public function setLogger (LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Set EventManager instance.
+     *
+     * @param EventManagerInterface $events Event manager
+     *
+     * @return void
+     */
+    public function setEventManager (EventManagerInterface $events)
+    {
+        $events->setIdentifiers('VuFind\Search');
+        $this->events = $events;
+    }
+
+    /**
+     * Return EventManager instance.
+     *
+     * Lazy loads a new EventManager if none was set.
+     *
+     * @return EventManagerInterface
+     */
+    public function getEventManager ()
+    {
+        if (!$this->events) {
+            $this->events = new EventManager('VuFind\Search');
+        }
+        return $this->events;
+    }
+
+    /// Internal API
+
+    /**
+     * Resolve a backend.
+     *
+     * @param string            $backend Backend name
+     * @param array|ArrayAccess $args    Service function arguments
+     *
+     * @return BackendInterface
+     *
+     * @throws Exception\RuntimeException Unable to resolve backend
+     */
+    protected function resolve ($backend, $args)
+    {
+        if (!isset($this->backends[$backend])) {
+            $response = $this->getEventManager()->trigger(
+                "resolve",
+                $this,
+                $args,
+                function ($o) {
+                    return ($o instanceOf BackendInterface);
+                }
+            );
+            if (!$response->stopped()) {
+                throw new Exception\RuntimeException(
+                    sprintf('Unable to resolve backend: %s, %s', $args['context'], $args['backend'])
+                );
+            }
+            $this->backends[$backend] = $response->last();
+        }
+        return $this->backends[$backend];
+    }
+
+    /**
+     * Trigger the pre event.
+     *
+     * @param string           $context Service context
+     * @param BackendInterface $backend Selected backend
+     * @param array            $args    Event arguments
+     *
+     * @return void
+     */
+    protected function triggerPre ($context, BackendInterface $backend, $args)
+    {
+        $this->getEventManager()->trigger($context . '.pre', $backend, $args);
+    }
+
+    /**
+     * Trigger the post event.
+     *
+     * @param string $context  Service context
+     * @param mixed  $response Backend response
+     * @param array  $args     Event arguments
+     *
+     * @return void
+     */
+    protected function triggerPost ($context, $response, $args)
+    {
+        $this->getEventManager()->trigger($context . '.post', $response, $args);
+    }
+
+    /**
+     * Send a message to the logger.
+     *
+     * @param string $level   Log level
+     * @param string $message Log message
+     * @param array  $context Log context
+     *
+     * @return void
+     */
+    protected function log ($level, $message, array $context = array())
+    {
+        if ($this->logger) {
+            $this->logger->$level($message, $context);
+        }
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/bootstrap.php b/module/VuFindSearch/tests/unit-tests/bootstrap.php
new file mode 100644
index 00000000000..0295fb3a5b0
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/bootstrap.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * Search subsystem PHPUnit bootstrap.
+ *
+ * @author    David Maus <maus@hab.de>
+ * @license   http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @copyright Copyright (C) Villanova University 2011
+ */
+
+require_once('Zend/Loader/AutoloaderFactory.php');
+\Zend\Loader\AutoloaderFactory::factory(
+    array(
+        'Zend\Loader\StandardAutoloader' => array(
+            'namespaces' => array(
+                'VuFindSearch' => realpath(__DIR__ . '/../../src/VuFindSearch'),
+                'VuFindTest' => realpath(__DIR__ . '/src/VuFindTest'),
+            ),
+            'autoregister_zf' => true
+        )
+    )
+);
+
+define('PHPUNIT_FIXTURES', realpath(__DIR__ . '/fixtures'));
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml b/module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml
new file mode 100644
index 00000000000..9f844caae2d
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/fixtures/searchspecs.yaml
@@ -0,0 +1,495 @@
+---
+# Listing of search types and their component parts and weights.
+#
+# Format is:
+#  searchType:
+#    # CustomMunge is an optional section to define custom pre-processing of
+#    #     user input.  See below for details of munge actions.
+#    CustomMunge:
+#      MungeName1:
+#        - [action1, actionParams]
+#        - [action2, actionParams]
+#        - [action3, actionParams]
+#      MungeName2:
+#        - [action1, actionParams]
+#    # DismaxFields is optional and defines the fields sent to the Dismax handler
+#    #     when we are able to use it.  QueryFields will be used for advanced
+#    #     searches that Dismax cannot support.  QueryFields is always used if no
+#    #     DismaxFields section is defined.
+#    DismaxFields:
+#      - field1^boost
+#      - field2^boost
+#      - field3^boost
+#    # DismaxParams is optional and allows you to override default Dismax settings
+#    #     (i.e. mm / bf) on a search-by-search basis.  If you want global default
+#    #     values for these settings, you can edit the "dismax" search handler in
+#    #     solr/biblio/conf/solrconfig.xml.
+#    DismaxParams:
+#      - [param1_name, param1_value]
+#      - [param2_name, param2_value]
+#      - [param3_name, param3_value]
+#    # QueryFields define the fields we are searching when not using Dismax
+#    QueryFields:
+#      - SolrField:
+#        - [howToMungeSearchstring, weight]
+#        - [differentMunge, weight]
+#      - DifferentSolrField:
+#        - [howToMunge, weight]
+#    # The optional FilterQuery section allows you to AND a static query to the
+#    #     dynamic query generated using the QueryFields; see JournalTitle below
+#    #     for an example.  This is applied whether we use DismaxFields or 
+#    #     QueryFields.
+#    FilterQuery: (optional Lucene filter query)
+# 
+# ...etc.
+#
+#-----------------------------------------------------------------------------------
+#
+# Within the QueryFields area, fields are OR'd together, unless they're in an 
+# anonymous array, in which case the first element is a two-value array that tells 
+# us what the type (AND or OR) and weight of the whole group should be.
+#
+# So, given:
+#
+# test:
+#   QueryFields:
+#     - A:
+#       - [onephrase, 500]
+#       - [and, 200]
+#     - B:
+#       - [and, 100]   
+#       - [or, 50]  
+#     # Start an anonymous array to group; first element indicates AND grouping 
+#     #     and a weight of 50
+#     -
+#       - [AND, 50]                 
+#       - C:
+#         - [onephrase, 200]
+#       - D:
+#         - [onephrase, 300]
+#       # Note the "not" attached to the field name as a minus, and the use of ~ 
+#       #     to mean null ("no special weight")
+#       - -E:
+#         - [or, ~]
+#     - D:
+#       - [or, 100]
+# 
+#  ...and the search string 
+#
+#      test "one two"
+#
+#  ...we'd get
+#   
+#   (A:"test one two"^500 OR 
+#    A:(test AND "one two")^ 200 OR
+#    B:(test AND "one two")^100 OR
+#    B:(test OR "one two")^50
+#    (
+#      C:("test one two")^200 AND
+#      D:"test one two"^300 AND
+#      -E:(test OR "one two")
+#    )^50 OR
+#    D:(test OR "one two")^100
+#   )
+#
+#-----------------------------------------------------------------------------------
+#
+# Munge types are based on the original Solr.php code, and consist of:
+#
+# onephrase: eliminate all quotes and do it as a single phrase. 
+#   testing "one two"
+#    ...becomes ("testing one two")
+#
+# and: AND the terms together
+#  testing "one two"
+#   ...becomes (testing AND "one two")
+#
+# or: OR the terms together
+#  testing "one two"
+#   ...becomes (testing OR "one two")
+#
+# Additional Munge types can be defined in the CustomMunge section.  Each array
+# entry under CustomMunge defines a new named munge type.  Each array entry under
+# the name of the munge type specifies a string manipulation operation.  Operations
+# will be applied in the order listed, and different operations take different
+# numbers of parameters.
+#
+# Munge operations:
+#
+# [append, text] - Append text to the end of the user's search string
+# [lowercase] - Convert string to lowercase
+# [preg_replace, pattern, replacement] - Perform a regular expression replace
+#     using the preg_replace() PHP function.  If you use backreferences in your
+#     replacement phrase, be sure to escape dollar signs (i.e. \$1, not $1).
+# [uppercase] - Convert string to uppercase
+#
+# See the CallNumber search below for an example of custom munging in action.
+#-----------------------------------------------------------------------------------
+
+# These searches use Dismax when possible:
+Author:
+  DismaxFields:
+    - author^100
+    - author_fuller^50
+    - author2
+    - author_additional
+  QueryFields:
+    - author:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    - author_fuller:
+      - [onephrase, 200]
+      - [and, 100]
+      - [or, 50]
+    - author2:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, ~]
+    - author_additional:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, ~]
+
+ISN:
+  DismaxFields:
+    - isbn
+    - issn
+  QueryFields:
+    - issn:
+      - [and, 100]
+      - [or, ~]
+    - isbn:
+      - [and, 100]
+      - [or, ~]
+
+Subject:
+  DismaxFields:
+    - topic_unstemmed^150
+    - topic^100
+    - geographic^50
+    - genre^50
+    - era
+  QueryFields:
+    - topic_unstemmed:
+      - [onephrase, 350]
+      - [and, 150]
+      - [or, ~]
+    - topic:
+      - [onephrase, 300]
+      - [and, 100]
+      - [or, ~]
+    - geographic:
+      - [onephrase, 300]
+      - [and, 100]
+      - [or, ~]
+    - genre:
+      - [onephrase, 300]
+      - [and, 100]
+      - [or, ~]
+    - era:
+      - [and, 100]
+      - [or, ~]
+
+# This field definition is a compromise that supports both journal-level and
+# article-level data.  The disadvantage is that hits in article titles will
+# be mixed in.  If you are building a purely article-oriented index, you should
+# customize this to remove all of the title_* fields and focus entirely on the
+# container_title field.
+JournalTitle:
+  DismaxFields:
+    - title_short^500
+    - title_full_unstemmed^450
+    - title_full^400
+    - title^300
+    - container_title^250
+    - title_alt^200
+    - title_new^100
+    - title_old
+    - series^100
+    - series2
+  QueryFields:
+    - title_short:
+      - [onephrase, 500]
+    - title_full_unstemmed:
+      - [onephrase, 450]
+      - [and, 400]
+    - title_full:
+      - [onephrase, 400]
+    - title:
+      - [onephrase, 300]
+      - [and, 250]
+    - container_title:
+      - [onephrase, 275]
+      - [and, 225]
+    - title_alt:
+      - [and, 200]
+    - title_new:
+      - [and, 100]
+    - title_old:
+      - [and, ~]  
+    - series:
+      - [onephrase, 100]
+      - [and, 50]
+    - series2:
+      - [onephrase, 50]
+      - [and , ~]
+  FilterQuery: "format:Journal OR format:Article"
+
+Title:
+  DismaxFields:
+    - title_short^500
+    - title_full_unstemmed^450
+    - title_full^400
+    - title^300
+    - title_alt^200
+    - title_new^100
+    - title_old
+    - series^100
+    - series2
+  QueryFields:
+    - title_short:
+      - [onephrase, 500]
+    - title_full_unstemmed:
+      - [onephrase, 450]
+      - [and, 400]
+    - title_full:
+      - [onephrase, 400]
+    - title:
+      - [onephrase, 300]
+      - [and, 250]
+    - title_alt:
+      - [and, 200]
+    - title_new:
+      - [and, 100]
+    - title_old:
+      - [and, ~]  
+    - series:
+      - [onephrase, 100]
+      - [and, 50]
+    - series2:
+      - [onephrase, 50]
+      - [and , ~]
+
+Series:
+  DismaxFields:
+    - series^100
+    - series2
+  QueryFields:
+    - series:
+      - [onephrase, 500]
+      - [and, 200]
+      - [or, 100]
+    - series2:
+      - [onephrase, 50]
+      - [and, 50]
+      - [or, ~]
+
+AllFields:
+  DismaxFields:
+    - title_short^750
+    - title_full_unstemmed^600
+    - title_full^400
+    - title^500
+    - title_alt^200
+    - title_new^100
+    - series^50
+    - series2^30
+    - author^300
+    - author_fuller^150
+    - contents^10
+    - topic_unstemmed^550
+    - topic^500
+    - geographic^300
+    - genre^300
+    - allfields_unstemmed^10
+    - fulltext_unstemmed^10
+    - allfields
+    - fulltext
+  QueryFields:
+    0:
+      - [OR, 50]
+      - title_short:
+        - [onephrase, 750]
+      - title_full_unstemmed:
+        - [onephrase, 600]
+        - [and, 500]
+      - title_full:
+        - [onephrase, 400]
+      - title:
+        - [onephrase, 300]
+        - [and, 250]
+      - title_alt:
+        - [and, 200]
+      - title_new:
+        - [and, 100]
+    series:
+      - [and, 50]
+    series2:
+      - [and, 30]
+    author:
+      - [onephrase, 300]
+      - [and, 250]
+    author_fuller:
+      - [onephrase, 150]
+      - [and, 125]
+    author2:
+      - [and, 50]
+    author_additional:
+      - [and, 50]
+    contents:
+      - [and, 10]
+    topic_unstemmed:
+      - [onephrase, 550]
+      - [and, 500]
+    topic:
+      - [onephrase, 500]
+    geographic:
+      - [onephrase, 300]
+    genre:
+      - [onephrase, 300]
+    allfields_unstemmed:
+      - [or, 10]
+    fulltext_unstemmed:
+      - [or, 10]
+    allfields:
+      - [or, ~]
+    fulltext:
+      - [or, ~]
+
+# These are advanced searches that never use Dismax:
+id:
+  QueryFields:
+    - id:
+      - [onephrase, ~]
+
+# Fields for exact matches originating from alphabetic browse
+ids:
+  QueryFields:
+    - id:
+      - [or, ~]
+
+TopicBrowse:
+  QueryFields:
+    - topic_browse:
+      - [onephrase, ~]
+
+AuthorBrowse:
+  QueryFields:
+    - author_browse:
+      - [onephrase, ~]
+
+TitleBrowse:
+  QueryFields:
+    - title_full:
+      - [onephrase, ~]
+
+DeweyBrowse:
+  QueryFields:
+    - dewey-raw:
+      - [onephrase, ~]
+
+LccBrowse:
+  QueryFields:
+    - callnumber-a:
+      - [onephrase, ~]
+
+CallNumber:
+  # We use two similar munges here -- one for exact matches, which will get
+  # a very high boost factor, and one for left-anchored wildcard searches,
+  # which will return a larger number of hits at a lower boost.
+  CustomMunge:
+    callnumber_exact:
+      - [uppercase]
+      # Strip whitespace and quotes:
+      - [preg_replace, '/[ "]/', ""]
+      # Escape colons (unescape first to avoid double-escapes):
+      - [preg_replace, '/(\\\:)/', ':']
+      - [preg_replace, '/:/', '\:']
+      # Strip pre-existing trailing asterisks:
+      - [preg_replace, "/\*+$/", ""]
+    callnumber_fuzzy:
+      - [uppercase]
+      # Strip whitespace and quotes:
+      - [preg_replace, '/[ "]/', ""]
+      # Escape colons (unescape first to avoid double-escapes):
+      - [preg_replace, '/(\\\:)/', ':']
+      - [preg_replace, '/:/', '\:']
+      # Strip pre-existing trailing asterisks:
+      - [preg_replace, "/\*+$/", ""]
+      # Ensure we have just one trailing asterisk.  The trailing space inside
+      #     the quotes has no effect on searching; it is a workaround for a
+      #     Horde::YAML parsing glitch -- see VUFIND-160 in JIRA for details.
+      - [append, "* "]
+  QueryFields:
+    - callnumber:
+      - [callnumber_exact, 1000]
+      - [callnumber_fuzzy, ~]
+    - dewey-full:
+      - [callnumber_exact, 1000]
+      - [callnumber_fuzzy, ~]
+
+publisher:
+  DismaxFields:
+    - publisher^100
+  QueryFields:
+    - publisher:
+      - [and, 100]
+      - [or, ~]
+
+year:
+  DismaxFields:
+    - publishDate^100
+  QueryFields:
+    - publishDate:
+      - [and, 100]
+      - [or, ~]
+
+language:
+  QueryFields:
+    - language:
+      - [and, ~]
+
+toc:
+  DismaxFields:
+    - contents^100
+  QueryFields:
+    - contents:
+      - [and, 100]
+      - [or, ~]
+
+topic:
+  QueryFields:
+    - topic:
+      - [and, 50]
+    - topic_facet:
+      - [and, ~]
+
+geographic:
+  QueryFields:
+    - geographic:
+      - [and, 50]
+    - geographic_facet:
+      - [and ~]
+
+genre:
+  QueryFields:
+    - genre:
+      - [and, 50]
+    - genre_facet:
+      - [and, ~]
+
+era:
+  QueryFields:
+    - era:
+      - [and, ~]
+
+oclc_num:
+  CustomMunge:
+    oclc_num:
+      - [preg_replace, "/[^0-9]/", ""]
+      # trim leading zeroes:
+      - [preg_replace, "/^0*/", ""]
+  QueryFields:
+    - oclc_num:
+      - [oclc_num, ~]
diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request
new file mode 100644
index 00000000000..d81a9ee6a77
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/bad-request
@@ -0,0 +1 @@
+HTTP/1.1 400 Bad Request
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error
new file mode 100644
index 00000000000..6432eb9f9c9
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/internal-server-error
@@ -0,0 +1 @@
+HTTP/1.1 500 Internal Server Error
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match
new file mode 100644
index 00000000000..7fc50f3b77d
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/no-match
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Date: Thu, 11 Oct 2012 07:56:30 GMT
+Last-Modified: Thu, 11 Oct 2012 07:05:29 GMT
+Server: Jetty(6.1.11)
+
+{"responseHeader":{"status":0,"QTime":0,"params":{"json.nl":"arrarr","wt":"json","q":"id:690xx0223"}},"response":{"numFound":0,"start":0,"docs":[]}}
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record
new file mode 100644
index 00000000000..c4478742955
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/fixtures/solr/response/single-record
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Date: Thu, 11 Oct 2012 07:56:30 GMT
+Last-Modified: Thu, 11 Oct 2012 07:05:29 GMT
+Server: Jetty(6.1.11)
+
+{"responseHeader":{"status":0,"QTime":0,"params":{"json.nl":"arrarr","wt":"json","q":"id:single-record"}},"response":{"numFound":1,"start":0,"docs":[{"illustrated":"Not Illustrated","id":"690250223","title":"Politics otherwise : Shakespeare as social and political critique /","title_sub":"Shakespeare as social and political critique /","spelling":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ... Amsterdam [u.a.] Rodopi 2012 XIV, 180 S. 23 cm Value inquiry book series 242 Philosophy. literature and politics Donskis, Leonidas (DE-601)391861484 (DE-601)357871030 (DE-600)14823147 Value inquiry book series 242","recordtype":"marc","title_auth":"Politics otherwise : Shakespeare as social and political critique /","title_short":"Politics otherwise :","title_sort":"politics otherwise :shakespeare as social and political critique","fullrecord":"00846nam a2200229 c 4500001001000000003000700010005001700017008004100034020002200075035002500097040002200122041000800144044001200152245010200164260003500266300002300301490007200324700004100396830007200437900005700509954005000566\u001e690250223\u001eDE-601\u001e20120419125548.0\u001e120419s2012    ne            000 0 eng d\u001e  \u001fa978-90-420-3464-8\u001e  \u001fa(DE-599)GBV690250223\u001e  \u001faGBVCP\u001fbger\u001ferakwb\u001e0 \u001faeng\u001e  \u001fane\u001faxxu\u001e10\u001faPolitics otherwise :\u001fbShakespeare as social and political critique /\u001fced. by Leonidas Donskis ...\u001e3 \u001faAmsterdam [u.a.]\u001fbRodopi\u001fc2012\u001e  \u001faXIV, 180 S.\u001fc23 cm\u001e0 \u001faValue inquiry book series\u001fv242\u001faPhilosophy. literature and politics\u001e1 \u001faDonskis, Leonidas\u001f0(DE-601)391861484\u001e  \u001fw(DE-601)357871030\u001fw(DE-600)14823147\u001faValue inquiry book series\u001fv242\u001e  \u001faGBV\u001fbHAB Wolfenbüttel <23>\u001fd62.2354\u001fxN\u001fzN\u001fd62.2354\u001e  \u001fa50\u001fb1305716574\u001fc01\u001fd62.2354\u001fea\u001fd62.2354\u001fx0023\u001e\u001d","title_full":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ...","title_fullStr":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ...","title_full_unstemmed":"Politics otherwise : Shakespeare as social and political critique / ed. by Leonidas Donskis ...","series":["Value inquiry book series"],"physical":["XIV, 180 S. 23 cm"],"building":["Library A"],"collection":["Catalog"],"format":["Book"],"series2":["Value inquiry book series","Philosophy. literature and politics"],"publisher":["Rodopi"],"ctrlnum":["(DE-599)GBV690250223"],"spellingShingle":["Value inquiry book series","Politics otherwise : Shakespeare as social and political critique /"],"isbn":["978-90-420-3464-8"],"author2Str":["Donskis, Leonidas"],"author2":["Donskis, Leonidas"],"language":["English"],"publishDate":["2012"],"institution":["MyInstitution"]}]}}
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/phpunit.xml b/module/VuFindSearch/tests/unit-tests/phpunit.xml
new file mode 100644
index 00000000000..fe5b7573e41
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/phpunit.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<phpunit bootstrap="bootstrap.php"
+         strict="true">
+  <testsuites>
+    <testsuite name="VuFindSearch">
+      <directory>src</directory>
+    </testsuite>
+  </testsuites>
+  <php>
+    <includePath>../../vendor/zf2/library</includePath>
+  </php>
+</phpunit>
diff --git a/module/VuFindSearch/tests/unit-tests/src/.keep b/module/VuFindSearch/tests/unit-tests/src/.keep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php
new file mode 100644
index 00000000000..93634163335
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * Unit tests for SOLR backend.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindTest\Backend\Solr;
+
+use VuFindSearch\Backend\Solr\Backend;
+use Zend\Http\Response;
+use PHPUnit_Framework_TestCase;
+use InvalidArgumentException;
+
+/**
+ * Unit tests for SOLR backend.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class BackendTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test retrieving a record.
+     *
+     * @return void
+     */
+    public function testRetrieve ()
+    {
+        $resp = $this->loadResponse('single-record');
+        $conn = $this->getMock('VuFindSearch\Backend\Solr\Connector', array('retrieve'), array('http://example.tld/'));
+        $conn->expects($this->once())
+            ->method('retrieve')
+            ->will($this->returnValue($resp->getBody()));
+
+        $back = new Backend('test', $conn);
+        $coll = $back->retrieve('foobar');
+        $this->assertCount(1, $coll);
+        $this->assertEquals('test', $coll->getSourceIdentifier());
+        $rec  = $coll->first();
+        $this->assertEquals('test', $rec->getSourceIdentifier());
+        $this->assertEquals('690250223', $rec->id);
+    }
+
+    /**
+     * Load a SOLR response as fixture.
+     *
+     * @param string $fixture Fixture file
+     *
+     * @return Zend\Http\Response
+     *
+     * @throws InvalidArgumentException Fixture files does not exist
+     */
+    protected function loadResponse ($fixture)
+    {
+        $file = realpath(sprintf('%s/solr/response/%s', PHPUNIT_FIXTURES, $fixture));
+        if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+            throw new InvalidArgumentException(sprintf('Unable to load fixture file: %s', $file));
+        }
+        return Response::fromString(file_get_contents($file));
+    }
+
+}
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php
new file mode 100644
index 00000000000..4a1d38496cb
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * Unit tests for SOLR connector.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindTest\Backend\Solr;
+
+use VuFindSearch\Backend\Solr\Connector;
+
+use Zend\Http\Client\Adapter\Test as TestAdapter;
+use Zend\Http\Client as HttpClient;
+
+use PHPUnit_Framework_TestCase;
+use InvalidArgumentException;
+
+/**
+ * Unit tests for SOLR connector.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class ConnectorTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Current response.
+     *
+     * @var string
+     */
+    protected $response;
+
+    /**
+     * Test record retrieval.
+     *
+     * @return void
+     */
+    public function testRetrieve ()
+    {
+        $conn = $this->createConnector('single-record');
+        $resp = $conn->retrieve('id');
+        $this->assertInternalType('string', $resp);
+        json_decode($resp, true);
+        $this->assertEquals(\JSON_ERROR_NONE, json_last_error());
+    }
+
+    /**
+     * Test retrieving a non-existent record returns a response.
+     *
+     * @return void
+     */
+    public function testRetrieveMissingRecord ()
+    {
+        $conn = $this->createConnector('no-match');
+        $resp = $conn->retrieve('id');
+        $this->assertInternalType('string', $resp);
+    }
+
+    /**
+     * Test RemoteErrorException is thrown on a remote 5xx error.
+     *
+     * @expectedException     VuFindSearch\Backend\Exception\RemoteErrorException
+     * @expectedExceptionCode 500
+     */
+    public function testInternalServerError ()
+    {
+        $conn = $this->createConnector('internal-server-error');
+        $resp = $conn->retrieve('id');
+    }
+
+    /**
+     * Test RequestErrorException is thrown on a remote 4xx error.
+     *
+     * @expectedException     VuFindSearch\Backend\Exception\RequestErrorException
+     * @expectedExceptionCode 400
+     */
+    public function testBadRequestError ()
+    {
+        $conn = $this->createConnector('bad-request');
+        $resp = $conn->retrieve('id');
+    }
+
+    /**
+     * Create connector with fixture file.
+     *
+     * @param string $fixture Fixture file
+     *
+     * @return Connector
+     *
+     * @throws InvalidArgumentException Fixture file does not exist
+     */
+    protected function createConnector ($fixture)
+    {
+        $file = realpath(sprintf('%s/solr/response/%s', PHPUNIT_FIXTURES, $fixture));
+        if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+            throw new InvalidArgumentException(sprintf('Unable to load fixture file: %s', $file));
+        }
+        $this->response = file_get_contents($file);
+
+        $conn = new Connector('http://example.tld/');
+        $conn->setProxy($this);
+        return $conn;
+    }
+
+    /**
+     * Set test adapter with prepared response.
+     *
+     * @param HttpClient $client HTTP client to mock
+     *
+     * @return void
+     */
+    public function proxify (HttpClient $client)
+    {
+        $adapter = new TestAdapter();
+        $adapter->setResponse($this->response);
+        $client->setAdapter($adapter);
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php
new file mode 100644
index 00000000000..0169a9b231f
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * Unit tests for SOLR query builder
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindTest\Backend\Solr;
+
+use VuFindSearch\Backend\Solr\QueryBuilder;
+use PHPUnit_Framework_TestCase;
+
+/**
+ * Unit tests for SOLR query builder
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class QueryBuilderTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test capitalizeBooleans functionality.
+     *
+     * @return void
+     */
+    public function testCapitalizeBooleans()
+    {
+        $qb = new QueryBuilder();
+
+        // Set up an array of expected inputs and outputs:
+        // @codingStandardsIgnoreStart
+        $tests = array(
+            array('this not that', 'this NOT that'),        // capitalize not
+            array('this and that', 'this AND that'),        // capitalize and
+            array('this or that', 'this OR that'),          // capitalize or
+            array('apples and oranges (not that)', 'apples AND oranges (NOT that)'),
+            array('"this not that"', '"this not that"'),    // do not capitalize inside quotes
+            array('"this and that"', '"this and that"'),    // do not capitalize inside quotes
+            array('"this or that"', '"this or that"'),      // do not capitalize inside quotes
+            array('"apples and oranges (not that)"', '"apples and oranges (not that)"'),
+            array('this AND that', 'this AND that'),        // don't mess up existing caps
+            array('and and and', 'and AND and'),
+            array('andornot noted andy oranges', 'andornot noted andy oranges'),
+            array('(this or that) and (apples not oranges)', '(this OR that) AND (apples NOT oranges)'),
+            array('this aNd that', 'this AND that'),        // strange capitalization of AND
+            array('this nOt that', 'this NOT that')         // strange capitalization of NOT
+        );
+        // @codingStandardsIgnoreEnd
+
+        // Test all the operations:
+        foreach ($tests as $current) {
+            $this->assertEquals(
+                $qb->capitalizeBooleans($current[0]), $current[1]
+            );
+        }
+    }
+
+    /**
+     * Test capitalizeRanges functionality.
+     *
+     * @return void
+     */
+    public function testCapitalizeRanges()
+    {
+        $qb = new QueryBuilder();
+
+        // Set up an array of expected inputs and outputs:
+        // @codingStandardsIgnoreStart
+        $tests = array(
+            array('"{a to b}"', '"{a to b}"'),              // don't capitalize inside quotes
+            array('"[a to b]"', '"[a to b]"'),
+            array('[a to b]', '([a TO b] OR [A TO B])'),    // expand alphabetic cases
+            array('[a TO b]', '([a TO b] OR [A TO B])'),
+            array('[a To b]', '([a TO b] OR [A TO B])'),
+            array('[a tO b]', '([a TO b] OR [A TO B])'),
+            array('{a to b}', '({a TO b} OR {A TO B})'),
+            array('{a TO b}', '({a TO b} OR {A TO B})'),
+            array('{a To b}', '({a TO b} OR {A TO B})'),
+            array('{a tO b}', '({a TO b} OR {A TO B})'),
+            array('[1900 to 1910]', '[1900 TO 1910]'),      // don't expand numeric cases
+            array('[1900 TO 1910]', '[1900 TO 1910]'),
+            array('{1900 to 1910}', '{1900 TO 1910}'),
+            array('{1900 TO 1910}', '{1900 TO 1910}'),
+            array('[a      to      b]', '([a TO b] OR [A TO B])'),   // handle extra spaces
+            // special case for timestamps:
+            array('[1900-01-01t00:00:00z to 1900-12-31t23:59:59z]', '[1900-01-01T00:00:00Z TO 1900-12-31T23:59:59Z]'),
+            array('{1900-01-01T00:00:00Z       TO   1900-12-31T23:59:59Z}', '{1900-01-01T00:00:00Z TO 1900-12-31T23:59:59Z}')
+        );
+        // @codingStandardsIgnoreEnd
+
+        // Test all the operations:
+        foreach ($tests as $current) {
+            $this->assertEquals(
+                $qb->capitalizeRanges($current[0]), $current[1]
+            );
+        }
+    }
+
+    /**
+     * Test parseRange functionality.
+     *
+     * @return void
+     */
+    public function testParseRange()
+    {
+        $this->markTestSkipped();
+        $qb = new QueryBuilder();
+
+        // basic range test:
+        $result = $qb->parseRange("[1 TO 100]");
+        $this->assertEquals('1', $result['from']);
+        $this->assertEquals('100', $result['to']);
+
+        // test whitespace handling:
+        $result = $qb->parseRange("[1      TO     100]");
+        $this->assertEquals('1', $result['from']);
+        $this->assertEquals('100', $result['to']);
+
+        // test invalid ranges:
+        $this->assertFalse($qb->parseRange('1 TO 100'));
+        $this->assertFalse($qb->parseRange('[not a range to me]'));
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php
new file mode 100644
index 00000000000..45198429e26
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * Unit tests for simple JSON-based record collection factory.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindTest\Backend\Solr\Json\Response;
+
+use VuFindSearch\Backend\Solr\Response\Json\RecordCollectionFactory;
+use PHPUnit_Framework_TestCase;
+
+/**
+ * Unit tests for simple JSON-based record collection factory.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class RecordCollectionFactoryTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test that the factory creates a collection.
+     *
+     * @return void
+     */
+    public function testFactory ()
+    {
+        $json = json_encode(array('response' => array('start' => 0, 'docs' => array(array(), array(), array()))));
+        $fact = new RecordCollectionFactory();
+        $coll = $fact->factory(json_decode($json, true));
+        $this->assertEquals(3, count($coll));
+    }
+}
\ No newline at end of file
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php
new file mode 100644
index 00000000000..0e5e02c07b3
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * Unit tests for SOLR search handler.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+
+namespace VuFindTest\Backend\Solr;
+
+use VuFindSearch\Backend\Solr\SearchHandler;
+use PHPUnit_Framework_TestCase;
+
+/**
+ * Unit tests for SOLR search handler.
+ *
+ * @category VuFind2
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org
+ */
+class SearchHandlerTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test creating simple dismax query.
+     *
+     * @return void
+     */
+    public function testSimpleSearchDismax ()
+    {
+        $spec = array('DismaxParams' => array(array('foo', 'bar')), 'DismaxFields' => array('field1', 'field2'));
+        $hndl = new SearchHandler($spec);
+        $this->assertEquals('(_query_:"{!dismax qf=\"field1 field2\" foo=\\\'bar\\\'}foobar")', $hndl->createSimpleQueryString('foobar'));
+    }
+}
-- 
GitLab