From 2011eba2bfe592a95f1fba3a80bc1411bc88de09 Mon Sep 17 00:00:00 2001
From: Robert Lange <robert.lange@uni-leipzig.de>
Date: Tue, 30 Jun 2020 16:08:29 +0200
Subject: [PATCH] refs #17714 [master] use inheritance for searchspecs.yaml

* prepare testing directory structure for yaml comparism
** add fixtures in same order as config files
** use symlinks for real configs
** use result searchspecs for old outcome before refactoring
* fix finc test setup for docker
* add finc SearchSpecsReaderTest
** run: docker exec -it [php container] sh -c "vendor/bin/phpunit --configuration module/finc/tests/phpunit.xml module/finc/tests/unit-tests/src/fincTest/Config/SearchSpecsReaderTest.php"
[--filter testParentYaml]"

* in local searchspecs.yaml
** use vufind as parent and keep only delta
* in local alpha searchspecs.yaml
** use local as parent, no delta

* use autoconfig version 2.3.3, see also https://git.sc.uni-leipzig.de/ubl/bdd_dev/webmasterei/autoconfig/-/commit/7b396da68c63820c869d45ca655d70d77b417aef
* add env var for local searchspecs.yaml in docker-env.0.finc.yml
---
 devops/docker/autoconfig/entrypoint.sh        |   2 +-
 docker-env.0.finc.yml                         |   2 +
 local/alpha/config/vufind/searchspecs.yaml    | 779 +-----------------
 local/config/vufind/searchspecs.yaml          | 141 +---
 module/finc/tests/bootstrap.php               |   5 +-
 .../config/vufind/searchspecs.yaml            |   1 +
 .../alpha/config/vufind/searchspecs.yaml      |   1 +
 .../local/config/vufind/searchspecs.yaml      |   1 +
 .../result/local/alpha/searchspecs.yaml       | 501 +++++++++++
 .../searchspecs/result/local/searchspecs.yaml | 501 +++++++++++
 module/finc/tests/phpunit.xml                 |   8 +
 .../fincTest/Config/SearchSpecsReaderTest.php | 133 +++
 12 files changed, 1157 insertions(+), 918 deletions(-)
 create mode 120000 module/finc/tests/fixtures/configs/yaml/searchspecs/config/vufind/searchspecs.yaml
 create mode 120000 module/finc/tests/fixtures/configs/yaml/searchspecs/local/alpha/config/vufind/searchspecs.yaml
 create mode 120000 module/finc/tests/fixtures/configs/yaml/searchspecs/local/config/vufind/searchspecs.yaml
 create mode 100644 module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/alpha/searchspecs.yaml
 create mode 100644 module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/searchspecs.yaml
 create mode 100644 module/finc/tests/unit-tests/src/fincTest/Config/SearchSpecsReaderTest.php

diff --git a/devops/docker/autoconfig/entrypoint.sh b/devops/docker/autoconfig/entrypoint.sh
index e8bcdc261a5..8213ec03d8a 100755
--- a/devops/docker/autoconfig/entrypoint.sh
+++ b/devops/docker/autoconfig/entrypoint.sh
@@ -18,7 +18,7 @@
 # @license https://opensource.org/licenses/GPL-3.0 GNU GPLv3
 
 if [ "$(whoami)" = "node" ]; then
-  [ -d "$HOME" ] || PREFIX=$HOME npm install -g @ubleipzig/autoconfig@2.3.2
+  [ -d "$HOME" ] || PREFIX=$HOME npm install -g @ubleipzig/autoconfig@2.3.3
   echo "$VUFIND_DEFAULTS" > "./data/docker/autoconfig/$VUFIND_SITE.json"
   exec "$HOME/bin/autoconfig" -c ./data/docker/autoconfig "$@"
   exit
diff --git a/docker-env.0.finc.yml b/docker-env.0.finc.yml
index d3766a42100..68452924bca 100644
--- a/docker-env.0.finc.yml
+++ b/docker-env.0.finc.yml
@@ -55,5 +55,7 @@ autoconfig:
         password: dev
     searches.ini:
       IndexShards: {}
+    searchspecs.yaml:
+      "@parent_yaml": "../../../config/vufind/searchspecs.yaml"
 
 mail: {}
\ No newline at end of file
diff --git a/local/alpha/config/vufind/searchspecs.yaml b/local/alpha/config/vufind/searchspecs.yaml
index a14e9f67517..0f5518b8304 100644
--- a/local/alpha/config/vufind/searchspecs.yaml
+++ b/local/alpha/config/vufind/searchspecs.yaml
@@ -1,779 +1,2 @@
 ---
-# 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. Enclose the parameter values
-#    #     in quotes for proper behavior. If you want global default values for these
-#    #     settings, you can edit the appropriate search handler in
-#    #     solr/biblio/conf/solrconfig.xml.
-#    DismaxParams:
-#      - [param1_name, param1_value]
-#      - [param2_name, param2_value]
-#      - [param3_name, param3_value]
-#    # This optional setting may be used to specify which Dismax handler to use. By
-#    #     default, VuFind provides two options: dismax (for the old, standard
-#    #     Dismax) and edismax (for Extended Dismax). You can also configure your own
-#    #     in solrconfig.xml, but VuFind relies on the name "edismax" to identify an
-#    #     Extended Dismax handler. If you omit this setting, the default value from
-#    #     the default_dismax_handler setting in the [Index] section of config.ini
-#    #     will be used.
-#    DismaxHandler: dismax|edismax
-#    # QueryFields define the fields we are searching when not using Dismax; VuFind
-#    #     detects queries that will not work with Dismax and switches to QueryFields
-#    #     as needed.
-#    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)
-#    ExactSettings:
-#      DismaxFields: ...
-#      QueryFields: ...
-#    # All the same settings as above, but for exact searches, i.e. search terms
-#    #     enclosed in quotes. Allows different fields or weights for exact
-#    #     searches. See below for commented-out examples.
-#
-# ...etc.
-#
-#-----------------------------------------------------------------------------------
-#
-# Within the QueryFields area, fields are OR'd together, unless they're in an
-# anonymous array with a numeric instead of alphanumeric key, 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
-#     0:
-#       0:
-#         - 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")
-#
-# identity: Use the search as-is
-#  testing "one two"
-#   ...becomes (testing "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:
-  DismaxParams:
-    - [bf , ord(publishDateSort)^10]
-  DismaxFields:
-    - author^400
-    - author2^300
-    - author_id^100
-    - author_ref^150
-    - author_corporate^200
-    - author_corporate2^200
-    - author_orig^200
-    - author2_orig^200
-    - author_corporate_orig^200
-    - author_corporate2_orig^200
-    - author_fuller^50
-    - author2_fuller
-    - author_additional
-    - author_variant
-    - author2_variant
-  QueryFields:
-    author:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_fuller:
-      - [onephrase, 200]
-      - [and, 100]
-      - [or, 50]
-    author2:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author_ref:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_corporate:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_corporate2:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_orig:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author2_orig:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_corporate_orig:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_corporate2_orig:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_id:
-      - [onephrase, 450]
-      - [and, 300]
-      - [or, 200]
-    author2_fuller:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author_additional:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author_variant:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author2_variant:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-
-ISN:
-  DismaxFields:
-    - isbn
-    - issn
-    - ismn
-  QueryFields:
-    issn:
-      - [and, 100]
-      - [or, ~]
-    isbn:
-      - [and, 100]
-      - [or, ~]
-    ismn:
-      - [and, 100]
-      - [or, ~]
-
-Signatur:
-#  DismaxParams:
-#    - [mm, 0]
-#  DismaxFields:
-#    - callnumber_ISIL
-  QueryFields:
-    callnumber_ISIL:
-      - [onephrase, 1000]
-      - [and, 100]
-      - [or, ~]
-
-Barcode:
-#  DismaxParams:
-#    - [mm, 0]
-#  DismaxFields:
-#    - barcode_ISIL
-  QueryFields:
-    barcode_ISIL:
-      - [onephrase, 1000]
-      - [and, 100]
-      - [or, ~]
-
-Subject:
-  DismaxFields:
-    - topic_unstemmed^150
-    - topic^100
-    - topic_id^100
-    - topic_ref^100
-    #- geographic^50
-    #- genre^50
-    #- era
-  QueryFields:
-    topic_unstemmed:
-      - [onephrase, 350]
-      - [and, 150]
-      - [or, ~]
-    topic:
-      - [onephrase, 300]
-      - [and, 100]
-      - [or, ~]
-    topic_ref:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    topic_id:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    #- geographic:
-    #  - [onephrase, 300]
-    #  - [and, 100]
-    #  - [or, ~]
-    #- genre:
-    #  - [onephrase, 300]
-    #  - [and, 100]
-    #  - [or, ~]
-    #- era:
-    #  - [and, 100]
-    #  - [or, ~]
-#  ExactSettings:
-#    DismaxFields:
-#      - topic_unstemmed^150
-#    QueryFields:
-#      - topic_unstemmed:
-#        - [onephrase, 350]
-#        - [and, 150]
-#        - [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 OR format:ElectronicBookPart"
-#  ExactSettings:
-#    DismaxFields:
-#      - title_full_unstemmed^450
-#    QueryFields:
-#      - title_full_unstemmed:
-#        - [onephrase, 450]
-#        - [and, 400]
-#    FilterQuery: "format:Journal OR format:Article"
-
-Title:
-  DismaxParams:
-    - [mm, 3]
-    - [bf , ord(publishDateSort)^10]
-  DismaxFields:
-#    - title_sub^200
-#    - title_short^300
-    - title_full_unstemmed^150
-    - title_full^100
-    - title^900
-    - title_alt^200
-    - title_new^100
-    - title_old
-    - title_orig^400
-    - series^100
-    - series2
-    - series_orig^100
-  QueryFields:
-    title_short:
-      - [onephrase, 500]
-    title_full_unstemmed:
-      - [onephrase, 150]
-      - [and, 100]
-    title_full:
-      - [onephrase, 100]
-    title:
-      - [onephrase, 300]
-      - [and, 250]
-    title_alt:
-      - [and, 200]
-    title_new:
-      - [and, 100]
-    title_old:
-      - [and, ~]
-    title_orig:
-      - [onephrase, 500]
-      - [and, 200]
-    series:
-      - [onephrase, 100]
-      - [and, 50]
-    series2:
-      - [onephrase, 50]
-      - [and , ~]
-    series_orig:
-      - [onephrase, 100]
-      - [and, 50]
-#  ExactSettings:
-#    DismaxFields:
-#      - title_full_unstemmed^450
-#    QueryFields:
-#      - title_full_unstemmed:
-#        - [onephrase, 450]
-#        - [and, 400]
-
-Series:
-  DismaxFields:
-    - series^100
-    - series2
-    - series_orig^100
-  QueryFields:
-    series:
-      - [onephrase, 500]
-      - [and, 200]
-      - [or, 100]
-    series2:
-      - [onephrase, 50]
-      - [and, 50]
-      - [or, ~]
-    series_orig:
-      - [onephrase, 500]
-      - [and, 200]
-      - [or, 100]
-
-Series2:
-  DismaxFields:
-    - series2
-  QueryFields:
-    series2:
-      - [onephrase, 200]
-      - [and, 50]
-
-AllFields:
-  DismaxParams:
-    - [mm, 3]
-    - [bf , ord(publishDateSort)^10]
-#    - [bf , "if(exists(query({!v='source_id:0'})),10,1)^1000"]
-    - [bf, "if(exists(query({!v='access_facet:Local*'})),10,1)^1000"]
-  DismaxFields:
-    - title_short^1000
-    - title_full_unstemmed^1000
-    - title_full^400
-    - title^500
-    - title_alt^200
-    - title_new^100
-    - title_orig^500
-    - series^50
-    - series2^30
-    - series_orig^50
-    - author^500
-    - author_fuller^150
-    - author_corporate^300
-    - author2^400
-    - author_corporate2^100
-    - author_ref^500
-    - author_orig^300
-    - author2_orig^300
-    - author_corporate_orig^300
-    - author_corporate2_orig^100
-    - topic_ref^10
-    - contents^10
-    - topic_unstemmed^15
-    - topic^10
-    - geographic^10
-    - genre^10
-    - rvk_label
-    - allfields_unstemmed^10
-    - allfields
-    - fulltext
-    - isbn
-    - issn
-    - ismn
-
-  QueryFields:
-    0:
-      0:
-        - OR
-        - 50
-      title_short:
-        - [onephrase, 1000]
-      title_full_unstemmed:
-        - [onephrase, 1000]
-        - [and, 500]
-      title_full:
-        - [onephrase, 400]
-      title:
-        - [onephrase, 300]
-        - [and, 250]
-      title_alt:
-        - [and, 200]
-      title_new:
-        - [and, 100]
-      title_orig:
-        - [onephrase, 500]
-        - [and, 400]
-    series:
-      - [onephrase, 300]
-      - [and, 100]
-    series2:
-      - [and, 30]
-    series_orig:
-      - [onephrase, 200]
-      - [and, 100]
-    author:
-      - [onephrase, 500]
-      - [and, 250]
-    author_fuller:
-      - [onephrase, 150]
-      - [and, 125]
-    author_ref:
-      - [onephrase, 250]
-      - [and, 250]
-      - [or, 250]
-    author_orig:
-      - [onephrase, 500]
-      - [and, 250]
-    author2_orig:
-      - [and, 50]
-    author_corporate_orig:
-      - [onephrase, 500]
-      - [and, 400]
-    author_corporate2_orig:
-      - [and, 50]
-    author_corporate:
-      - [onephrase, 500]
-      - [and, 400]
-    author2:
-      - [and, 50]
-    author_additional:
-      - [and, 50]
-    author_corporate2:
-      - [and, 50]
-    contents:
-      - [and, 10]
-    topic_unstemmed:
-      - [onephrase, 55]
-      - [and, 50]
-    topic:
-      - [onephrase, 50]
-    topic_ref:
-      - [onephrase, 10]
-      - [and, 5]
-      - [or, 5]
-    topic_id:
-      - [onephrase, 50]
-      - [and, 25]
-    allfields_unstemmed:
-      - [or, 10]
-#    fulltext_unstemmed:
-#      - [or, 10]
-    allfields:
-      - [or, ~]
-    fulltext:
-      - [or, ~]
-#    description:
-#      - [or, ~]
-    rvk_label:
-      - [onephrase, 500]
-      - [and, 250]
-      - [or, 250]
-    isbn:
-      - [onephrase, 500]
-    issn:
-      - [onephrase, 500]
-    ismn:
-      - [onephrase, 500]
-    imprint:
-      - [onephrase, 500]
-      
-#  ExactSettings:
-#    DismaxFields:
-#      - title_full_unstemmed^600
-#      - topic_unstemmed^550
-#      - allfields_unstemmed^10
-#      - fulltext_unstemmed^10
-#      - isbn
-#      - issn
-#    QueryFields:
-#      title_full_unstemmed:
-#        - [onephrase, 600]
-#        - [and, 500]
-#      topic_unstemmed:
-#        - [onephrase, 550]
-#        - [and, 500]
-#      allfields_unstemmed:
-#        - [or, 10]
-#      fulltext_unstemmed:
-#        - [or, 10]
-#      isbn:
-#        - [onephrase, ~]
-#      issn:
-#        - [onephrase, ~]
-
-# These are advanced searches that never use Dismax:
-id:
-  QueryFields:
-    id:
-      - [onephrase, ~]
-
-ParentID:
-  QueryFields:
-    hierarchy_parent_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, then add a new one:
-      #- [preg_replace, '/\*+$/', '']
-      #- [append, "*"]
-  QueryFields:
-    callnumber-search:
-      - [callnumber_exact, 1000]
-      - [callnumber_fuzzy, ~]
-    dewey-search:
-      - [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, ~]
-      
-rvk:
-  DismaxFields:
-    - rvk_facet^100
-  QueryFields:
-    rvk_facet:
-      - [and, 50]
-      - [or, 50]
-      
-rvk_path:
-  QueryFields:
-    rvk_path:
-      - [onephrase, ~]
-
-multipart:
-  DismaxFields:
-    - multipart_link^100
-  QueryFields:
-    multipart_link:
-      - [and, 50]
-      - [or, 50]  
-
-titleUniform:
-  QueryFields:
-    title_id_str_mv:
-      - [onephrase, ~]
\ No newline at end of file
+"@parent_yaml": "../../../config/vufind/searchspecs.yaml"
\ No newline at end of file
diff --git a/local/config/vufind/searchspecs.yaml b/local/config/vufind/searchspecs.yaml
index 04693c444c6..aa857af2a83 100644
--- a/local/config/vufind/searchspecs.yaml
+++ b/local/config/vufind/searchspecs.yaml
@@ -150,6 +150,8 @@
 # See the CallNumber search below for an example of custom munging in action.
 #-----------------------------------------------------------------------------------
 
+"@parent_yaml": "../../../config/vufind/searchspecs.yaml"
+
 # These searches use Dismax when possible:
 Author:
   DismaxParams:
@@ -352,7 +354,7 @@ JournalTitle:
     title_new:
       - [and, 100]
     title_old:
-      - [and, ~]  
+      - [and, ~]
     series:
       - [onephrase, 100]
       - [and, 50]
@@ -611,147 +613,12 @@ AllFields:
 #      issn:
 #        - [onephrase, ~]
 
-# These are advanced searches that never use Dismax:
-id:
-  QueryFields:
-    id:
-      - [onephrase, ~]
-
-ParentID:
-  QueryFields:
-    hierarchy_parent_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, ~]
-
+# These are advanced searches that never use Dismax (finc customized):
 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, then add a new one:
-      #- [preg_replace, '/\*+$/', '']
-      #- [append, "*"]
-  #QueryFields:
-    #callnumber-search:
-      #- [callnumber_exact, 1000]
-      #- [callnumber_fuzzy, ~]
-    #dewey-search:
-      #- [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, ~]
-      
 rvk:
   DismaxFields:
     - rvk_facet^100
diff --git a/module/finc/tests/bootstrap.php b/module/finc/tests/bootstrap.php
index a03458275c1..62190b89ea0 100644
--- a/module/finc/tests/bootstrap.php
+++ b/module/finc/tests/bootstrap.php
@@ -101,7 +101,8 @@ class Bootstrap
         ];
         self::initEnvironment();
         $config = ArrayUtils::merge($baseConfig, $testConfig);
-        $serviceManager = new ServiceManager(new ServiceManagerConfig());
+        $managerConfig = new ServiceManagerConfig();
+        $serviceManager = new ServiceManager($managerConfig->toArray());
         $serviceManager->setService('ApplicationConfig', $config);
         $serviceManager->get('ModuleManager')->loadModules();
         static::$serviceManager = $serviceManager;
@@ -113,7 +114,7 @@ class Bootstrap
      *
      * @return void
      */
-    public function initEnvironment()
+    public static function initEnvironment()
     {
         define('APPLICATION_ENV', 'development');
         define('FINC_TEST_FIXTURES', realpath(FINC_TESTS_PATH . '/fixtures'));
diff --git a/module/finc/tests/fixtures/configs/yaml/searchspecs/config/vufind/searchspecs.yaml b/module/finc/tests/fixtures/configs/yaml/searchspecs/config/vufind/searchspecs.yaml
new file mode 120000
index 00000000000..183f7ab71ef
--- /dev/null
+++ b/module/finc/tests/fixtures/configs/yaml/searchspecs/config/vufind/searchspecs.yaml
@@ -0,0 +1 @@
+../../../../../../../../../config/vufind/searchspecs.yaml
\ No newline at end of file
diff --git a/module/finc/tests/fixtures/configs/yaml/searchspecs/local/alpha/config/vufind/searchspecs.yaml b/module/finc/tests/fixtures/configs/yaml/searchspecs/local/alpha/config/vufind/searchspecs.yaml
new file mode 120000
index 00000000000..59ae9378e2e
--- /dev/null
+++ b/module/finc/tests/fixtures/configs/yaml/searchspecs/local/alpha/config/vufind/searchspecs.yaml
@@ -0,0 +1 @@
+../../../../../../../../../../../local/alpha/config/vufind/searchspecs.yaml
\ No newline at end of file
diff --git a/module/finc/tests/fixtures/configs/yaml/searchspecs/local/config/vufind/searchspecs.yaml b/module/finc/tests/fixtures/configs/yaml/searchspecs/local/config/vufind/searchspecs.yaml
new file mode 120000
index 00000000000..74dc0578aea
--- /dev/null
+++ b/module/finc/tests/fixtures/configs/yaml/searchspecs/local/config/vufind/searchspecs.yaml
@@ -0,0 +1 @@
+../../../../../../../../../../local/config/vufind/searchspecs.yaml
\ No newline at end of file
diff --git a/module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/alpha/searchspecs.yaml b/module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/alpha/searchspecs.yaml
new file mode 100644
index 00000000000..572b27d2666
--- /dev/null
+++ b/module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/alpha/searchspecs.yaml
@@ -0,0 +1,501 @@
+Author:
+  DismaxParams:
+    -
+      - bf
+      - ord(publishDateSort)^10
+  DismaxFields:
+    - author^400
+    - author2^300
+    - author_id^100
+    - author_ref^150
+    - author_corporate^200
+    - author_corporate2^200
+    - author_orig^200
+    - author2_orig^200
+    - author_corporate_orig^200
+    - author_corporate2_orig^200
+    - author_fuller^50
+    - author2_fuller
+    - author_additional
+    - author_variant
+    - author2_variant
+  QueryFields:
+    author:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_fuller:
+      - [onephrase, 200]
+      - [and, 100]
+      - [or, 50]
+    author2:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author_ref:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate2:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author2_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate2_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_id:
+      - [onephrase, 450]
+      - [and, 300]
+      - [or, 200]
+    author2_fuller:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author_additional:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author_variant:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author2_variant:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+ISN:
+  DismaxFields:
+    - isbn
+    - issn
+    - ismn
+  QueryFields:
+    issn:
+      - [and, 100]
+      - [or, null]
+    isbn:
+      - [and, 100]
+      - [or, null]
+    ismn:
+      - [and, 100]
+      - [or, null]
+Signatur:
+  QueryFields:
+    callnumber_ISIL:
+      - [onephrase, 1000]
+      - [and, 100]
+      - [or, null]
+Barcode:
+  QueryFields:
+    barcode_ISIL:
+      - [onephrase, 1000]
+      - [and, 100]
+      - [or, null]
+Subject:
+  DismaxFields:
+    - topic_unstemmed^150
+    - topic^100
+    - topic_id^100
+    - topic_ref^100
+  QueryFields:
+    topic_unstemmed:
+      - [onephrase, 350]
+      - [and, 150]
+      - [or, null]
+    topic:
+      - [onephrase, 300]
+      - [and, 100]
+      - [or, null]
+    topic_ref:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    topic_id:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+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, null]
+    series:
+      - [onephrase, 100]
+      - [and, 50]
+    series2:
+      - [onephrase, 50]
+      - [and, null]
+  FilterQuery: 'format:Journal OR format:Article OR format:ElectronicBookPart'
+Title:
+  DismaxParams:
+    -
+      - mm
+      - 3
+    -
+      - bf
+      - ord(publishDateSort)^10
+  DismaxFields:
+    - title_full_unstemmed^150
+    - title_full^100
+    - title^900
+    - title_alt^200
+    - title_new^100
+    - title_old
+    - title_orig^400
+    - series^100
+    - series2
+    - series_orig^100
+  QueryFields:
+    title_short:
+      - [onephrase, 500]
+    title_full_unstemmed:
+      - [onephrase, 150]
+      - [and, 100]
+    title_full:
+      - [onephrase, 100]
+    title:
+      - [onephrase, 300]
+      - [and, 250]
+    title_alt:
+      - [and, 200]
+    title_new:
+      - [and, 100]
+    title_old:
+      - [and, null]
+    title_orig:
+      - [onephrase, 500]
+      - [and, 200]
+    series:
+      - [onephrase, 100]
+      - [and, 50]
+    series2:
+      - [onephrase, 50]
+      - [and, null]
+    series_orig:
+      - [onephrase, 100]
+      - [and, 50]
+Series:
+  DismaxFields:
+    - series^100
+    - series2
+    - series_orig^100
+  QueryFields:
+    series:
+      - [onephrase, 500]
+      - [and, 200]
+      - [or, 100]
+    series2:
+      - [onephrase, 50]
+      - [and, 50]
+      - [or, null]
+    series_orig:
+      - [onephrase, 500]
+      - [and, 200]
+      - [or, 100]
+Series2:
+  DismaxFields:
+    - series2
+  QueryFields:
+    series2:
+      - [onephrase, 200]
+      - [and, 50]
+AllFields:
+  DismaxParams:
+    -
+      - mm
+      - 3
+    -
+      - bf
+      - ord(publishDateSort)^10
+    -
+      - bf
+      - 'if(exists(query({!v=''access_facet:Local*''})),10,1)^1000'
+  DismaxFields:
+    - title_short^1000
+    - title_full_unstemmed^1000
+    - title_full^400
+    - title^500
+    - title_alt^200
+    - title_new^100
+    - title_orig^500
+    - series^50
+    - series2^30
+    - series_orig^50
+    - author^500
+    - author_fuller^150
+    - author_corporate^300
+    - author2^400
+    - author_corporate2^100
+    - author_ref^500
+    - author_orig^300
+    - author2_orig^300
+    - author_corporate_orig^300
+    - author_corporate2_orig^100
+    - topic_ref^10
+    - contents^10
+    - topic_unstemmed^15
+    - topic^10
+    - geographic^10
+    - genre^10
+    - rvk_label
+    - allfields_unstemmed^10
+    - allfields
+    - fulltext
+    - isbn
+    - issn
+    - ismn
+  QueryFields:
+    0:
+      0: [OR, 50]
+      title_short: [[onephrase, 1000]]
+      title_full_unstemmed: [[onephrase, 1000], [and, 500]]
+      title_full: [[onephrase, 400]]
+      title: [[onephrase, 300], [and, 250]]
+      title_alt: [[and, 200]]
+      title_new: [[and, 100]]
+      title_orig: [[onephrase, 500], [and, 400]]
+    series:
+      - [onephrase, 300]
+      - [and, 100]
+    series2:
+      - [and, 30]
+    series_orig:
+      - [onephrase, 200]
+      - [and, 100]
+    author:
+      - [onephrase, 500]
+      - [and, 250]
+    author_fuller:
+      - [onephrase, 150]
+      - [and, 125]
+    author_ref:
+      - [onephrase, 250]
+      - [and, 250]
+      - [or, 250]
+    author_orig:
+      - [onephrase, 500]
+      - [and, 250]
+    author2_orig:
+      - [and, 50]
+    author_corporate_orig:
+      - [onephrase, 500]
+      - [and, 400]
+    author_corporate2_orig:
+      - [and, 50]
+    author_corporate:
+      - [onephrase, 500]
+      - [and, 400]
+    author2:
+      - [and, 50]
+    author_additional:
+      - [and, 50]
+    author_corporate2:
+      - [and, 50]
+    contents:
+      - [and, 10]
+    topic_unstemmed:
+      - [onephrase, 55]
+      - [and, 50]
+    topic:
+      - [onephrase, 50]
+    topic_ref:
+      - [onephrase, 10]
+      - [and, 5]
+      - [or, 5]
+    topic_id:
+      - [onephrase, 50]
+      - [and, 25]
+    allfields_unstemmed:
+      - [or, 10]
+    allfields:
+      - [or, null]
+    fulltext:
+      - [or, null]
+    rvk_label:
+      - [onephrase, 500]
+      - [and, 250]
+      - [or, 250]
+    isbn:
+      - [onephrase, 500]
+    issn:
+      - [onephrase, 500]
+    ismn:
+      - [onephrase, 500]
+    imprint:
+      - [onephrase, 500]
+id:
+  QueryFields:
+    id:
+      - [onephrase, null]
+ParentID:
+  QueryFields:
+    hierarchy_parent_id:
+      - [onephrase, null]
+ids:
+  QueryFields:
+    id:
+      - [or, null]
+TopicBrowse:
+  QueryFields:
+    topic_browse:
+      - [onephrase, null]
+AuthorBrowse:
+  QueryFields:
+    author_browse:
+      - [onephrase, null]
+TitleBrowse:
+  QueryFields:
+    title_full:
+      - [onephrase, null]
+DeweyBrowse:
+  QueryFields:
+    dewey-raw:
+      - [onephrase, null]
+LccBrowse:
+  QueryFields:
+    callnumber-a:
+      - [onephrase, null]
+publisher:
+  DismaxFields:
+    - publisher^100
+  QueryFields:
+    publisher:
+      - [and, 100]
+      - [or, null]
+year:
+  DismaxFields:
+    - publishDate^100
+  QueryFields:
+    publishDate:
+      - [and, 100]
+      - [or, null]
+language:
+  QueryFields:
+    language:
+      - [and, null]
+toc:
+  DismaxFields:
+    - contents^100
+  QueryFields:
+    contents:
+      - [and, 100]
+      - [or, null]
+topic:
+  QueryFields:
+    topic:
+      - [and, 50]
+    topic_facet:
+      - [and, null]
+geographic:
+  QueryFields:
+    geographic:
+      - [and, 50]
+    geographic_facet:
+      - [and, null]
+genre:
+  QueryFields:
+    genre:
+      - [and, 50]
+    genre_facet:
+      - [and, null]
+era:
+  QueryFields:
+    era:
+      - [and, null]
+oclc_num:
+  CustomMunge:
+    oclc_num:
+      - [preg_replace, '/[^0-9]/', '']
+      - [preg_replace, '/^0*/', '']
+  QueryFields:
+    oclc_num:
+      - [oclc_num, null]
+rvk:
+  DismaxFields:
+    - rvk_facet^100
+  QueryFields:
+    rvk_facet:
+      - [and, 50]
+      - [or, 50]
+rvk_path:
+  QueryFields:
+    rvk_path:
+      - [onephrase, null]
+multipart:
+  DismaxFields:
+    - multipart_link^100
+  QueryFields:
+    multipart_link:
+      - [and, 50]
+      - [or, 50]
+titleUniform:
+  QueryFields:
+    title_id_str_mv:
+      - [onephrase, null]
+Coordinate:
+  DismaxFields:
+    - long_lat_display
+  DismaxHandler: edismax
+CallNumber:
+  CustomMunge:
+    callnumber_exact:
+      - [preg_replace, '/[ "]/', '']
+      - [preg_replace, '/(\\:)/', ':']
+      - [preg_replace, '/:/', '\:']
+      - [preg_replace, '/\*+$/', '']
+    callnumber_fuzzy:
+      - [preg_replace, '/[ "]/', '']
+      - [preg_replace, '/(\\:)/', ':']
+      - [preg_replace, '/:/', '\:']
+      - [preg_replace, '/\*+$/', '']
+      - [append, '*']
+  QueryFields:
+    callnumber-search:
+      - [callnumber_exact, 1000]
+      - [callnumber_fuzzy, null]
+    dewey-search:
+      - [callnumber_exact, 1000]
+      - [callnumber_fuzzy, null]
diff --git a/module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/searchspecs.yaml b/module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/searchspecs.yaml
new file mode 100644
index 00000000000..572b27d2666
--- /dev/null
+++ b/module/finc/tests/fixtures/configs/yaml/searchspecs/result/local/searchspecs.yaml
@@ -0,0 +1,501 @@
+Author:
+  DismaxParams:
+    -
+      - bf
+      - ord(publishDateSort)^10
+  DismaxFields:
+    - author^400
+    - author2^300
+    - author_id^100
+    - author_ref^150
+    - author_corporate^200
+    - author_corporate2^200
+    - author_orig^200
+    - author2_orig^200
+    - author_corporate_orig^200
+    - author_corporate2_orig^200
+    - author_fuller^50
+    - author2_fuller
+    - author_additional
+    - author_variant
+    - author2_variant
+  QueryFields:
+    author:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_fuller:
+      - [onephrase, 200]
+      - [and, 100]
+      - [or, 50]
+    author2:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author_ref:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate2:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author2_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_corporate2_orig:
+      - [onephrase, 350]
+      - [and, 200]
+      - [or, 100]
+    author_id:
+      - [onephrase, 450]
+      - [and, 300]
+      - [or, 200]
+    author2_fuller:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author_additional:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author_variant:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    author2_variant:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+ISN:
+  DismaxFields:
+    - isbn
+    - issn
+    - ismn
+  QueryFields:
+    issn:
+      - [and, 100]
+      - [or, null]
+    isbn:
+      - [and, 100]
+      - [or, null]
+    ismn:
+      - [and, 100]
+      - [or, null]
+Signatur:
+  QueryFields:
+    callnumber_ISIL:
+      - [onephrase, 1000]
+      - [and, 100]
+      - [or, null]
+Barcode:
+  QueryFields:
+    barcode_ISIL:
+      - [onephrase, 1000]
+      - [and, 100]
+      - [or, null]
+Subject:
+  DismaxFields:
+    - topic_unstemmed^150
+    - topic^100
+    - topic_id^100
+    - topic_ref^100
+  QueryFields:
+    topic_unstemmed:
+      - [onephrase, 350]
+      - [and, 150]
+      - [or, null]
+    topic:
+      - [onephrase, 300]
+      - [and, 100]
+      - [or, null]
+    topic_ref:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+    topic_id:
+      - [onephrase, 100]
+      - [and, 50]
+      - [or, null]
+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, null]
+    series:
+      - [onephrase, 100]
+      - [and, 50]
+    series2:
+      - [onephrase, 50]
+      - [and, null]
+  FilterQuery: 'format:Journal OR format:Article OR format:ElectronicBookPart'
+Title:
+  DismaxParams:
+    -
+      - mm
+      - 3
+    -
+      - bf
+      - ord(publishDateSort)^10
+  DismaxFields:
+    - title_full_unstemmed^150
+    - title_full^100
+    - title^900
+    - title_alt^200
+    - title_new^100
+    - title_old
+    - title_orig^400
+    - series^100
+    - series2
+    - series_orig^100
+  QueryFields:
+    title_short:
+      - [onephrase, 500]
+    title_full_unstemmed:
+      - [onephrase, 150]
+      - [and, 100]
+    title_full:
+      - [onephrase, 100]
+    title:
+      - [onephrase, 300]
+      - [and, 250]
+    title_alt:
+      - [and, 200]
+    title_new:
+      - [and, 100]
+    title_old:
+      - [and, null]
+    title_orig:
+      - [onephrase, 500]
+      - [and, 200]
+    series:
+      - [onephrase, 100]
+      - [and, 50]
+    series2:
+      - [onephrase, 50]
+      - [and, null]
+    series_orig:
+      - [onephrase, 100]
+      - [and, 50]
+Series:
+  DismaxFields:
+    - series^100
+    - series2
+    - series_orig^100
+  QueryFields:
+    series:
+      - [onephrase, 500]
+      - [and, 200]
+      - [or, 100]
+    series2:
+      - [onephrase, 50]
+      - [and, 50]
+      - [or, null]
+    series_orig:
+      - [onephrase, 500]
+      - [and, 200]
+      - [or, 100]
+Series2:
+  DismaxFields:
+    - series2
+  QueryFields:
+    series2:
+      - [onephrase, 200]
+      - [and, 50]
+AllFields:
+  DismaxParams:
+    -
+      - mm
+      - 3
+    -
+      - bf
+      - ord(publishDateSort)^10
+    -
+      - bf
+      - 'if(exists(query({!v=''access_facet:Local*''})),10,1)^1000'
+  DismaxFields:
+    - title_short^1000
+    - title_full_unstemmed^1000
+    - title_full^400
+    - title^500
+    - title_alt^200
+    - title_new^100
+    - title_orig^500
+    - series^50
+    - series2^30
+    - series_orig^50
+    - author^500
+    - author_fuller^150
+    - author_corporate^300
+    - author2^400
+    - author_corporate2^100
+    - author_ref^500
+    - author_orig^300
+    - author2_orig^300
+    - author_corporate_orig^300
+    - author_corporate2_orig^100
+    - topic_ref^10
+    - contents^10
+    - topic_unstemmed^15
+    - topic^10
+    - geographic^10
+    - genre^10
+    - rvk_label
+    - allfields_unstemmed^10
+    - allfields
+    - fulltext
+    - isbn
+    - issn
+    - ismn
+  QueryFields:
+    0:
+      0: [OR, 50]
+      title_short: [[onephrase, 1000]]
+      title_full_unstemmed: [[onephrase, 1000], [and, 500]]
+      title_full: [[onephrase, 400]]
+      title: [[onephrase, 300], [and, 250]]
+      title_alt: [[and, 200]]
+      title_new: [[and, 100]]
+      title_orig: [[onephrase, 500], [and, 400]]
+    series:
+      - [onephrase, 300]
+      - [and, 100]
+    series2:
+      - [and, 30]
+    series_orig:
+      - [onephrase, 200]
+      - [and, 100]
+    author:
+      - [onephrase, 500]
+      - [and, 250]
+    author_fuller:
+      - [onephrase, 150]
+      - [and, 125]
+    author_ref:
+      - [onephrase, 250]
+      - [and, 250]
+      - [or, 250]
+    author_orig:
+      - [onephrase, 500]
+      - [and, 250]
+    author2_orig:
+      - [and, 50]
+    author_corporate_orig:
+      - [onephrase, 500]
+      - [and, 400]
+    author_corporate2_orig:
+      - [and, 50]
+    author_corporate:
+      - [onephrase, 500]
+      - [and, 400]
+    author2:
+      - [and, 50]
+    author_additional:
+      - [and, 50]
+    author_corporate2:
+      - [and, 50]
+    contents:
+      - [and, 10]
+    topic_unstemmed:
+      - [onephrase, 55]
+      - [and, 50]
+    topic:
+      - [onephrase, 50]
+    topic_ref:
+      - [onephrase, 10]
+      - [and, 5]
+      - [or, 5]
+    topic_id:
+      - [onephrase, 50]
+      - [and, 25]
+    allfields_unstemmed:
+      - [or, 10]
+    allfields:
+      - [or, null]
+    fulltext:
+      - [or, null]
+    rvk_label:
+      - [onephrase, 500]
+      - [and, 250]
+      - [or, 250]
+    isbn:
+      - [onephrase, 500]
+    issn:
+      - [onephrase, 500]
+    ismn:
+      - [onephrase, 500]
+    imprint:
+      - [onephrase, 500]
+id:
+  QueryFields:
+    id:
+      - [onephrase, null]
+ParentID:
+  QueryFields:
+    hierarchy_parent_id:
+      - [onephrase, null]
+ids:
+  QueryFields:
+    id:
+      - [or, null]
+TopicBrowse:
+  QueryFields:
+    topic_browse:
+      - [onephrase, null]
+AuthorBrowse:
+  QueryFields:
+    author_browse:
+      - [onephrase, null]
+TitleBrowse:
+  QueryFields:
+    title_full:
+      - [onephrase, null]
+DeweyBrowse:
+  QueryFields:
+    dewey-raw:
+      - [onephrase, null]
+LccBrowse:
+  QueryFields:
+    callnumber-a:
+      - [onephrase, null]
+publisher:
+  DismaxFields:
+    - publisher^100
+  QueryFields:
+    publisher:
+      - [and, 100]
+      - [or, null]
+year:
+  DismaxFields:
+    - publishDate^100
+  QueryFields:
+    publishDate:
+      - [and, 100]
+      - [or, null]
+language:
+  QueryFields:
+    language:
+      - [and, null]
+toc:
+  DismaxFields:
+    - contents^100
+  QueryFields:
+    contents:
+      - [and, 100]
+      - [or, null]
+topic:
+  QueryFields:
+    topic:
+      - [and, 50]
+    topic_facet:
+      - [and, null]
+geographic:
+  QueryFields:
+    geographic:
+      - [and, 50]
+    geographic_facet:
+      - [and, null]
+genre:
+  QueryFields:
+    genre:
+      - [and, 50]
+    genre_facet:
+      - [and, null]
+era:
+  QueryFields:
+    era:
+      - [and, null]
+oclc_num:
+  CustomMunge:
+    oclc_num:
+      - [preg_replace, '/[^0-9]/', '']
+      - [preg_replace, '/^0*/', '']
+  QueryFields:
+    oclc_num:
+      - [oclc_num, null]
+rvk:
+  DismaxFields:
+    - rvk_facet^100
+  QueryFields:
+    rvk_facet:
+      - [and, 50]
+      - [or, 50]
+rvk_path:
+  QueryFields:
+    rvk_path:
+      - [onephrase, null]
+multipart:
+  DismaxFields:
+    - multipart_link^100
+  QueryFields:
+    multipart_link:
+      - [and, 50]
+      - [or, 50]
+titleUniform:
+  QueryFields:
+    title_id_str_mv:
+      - [onephrase, null]
+Coordinate:
+  DismaxFields:
+    - long_lat_display
+  DismaxHandler: edismax
+CallNumber:
+  CustomMunge:
+    callnumber_exact:
+      - [preg_replace, '/[ "]/', '']
+      - [preg_replace, '/(\\:)/', ':']
+      - [preg_replace, '/:/', '\:']
+      - [preg_replace, '/\*+$/', '']
+    callnumber_fuzzy:
+      - [preg_replace, '/[ "]/', '']
+      - [preg_replace, '/(\\:)/', ':']
+      - [preg_replace, '/:/', '\:']
+      - [preg_replace, '/\*+$/', '']
+      - [append, '*']
+  QueryFields:
+    callnumber-search:
+      - [callnumber_exact, 1000]
+      - [callnumber_fuzzy, null]
+    dewey-search:
+      - [callnumber_exact, 1000]
+      - [callnumber_fuzzy, null]
diff --git a/module/finc/tests/phpunit.xml b/module/finc/tests/phpunit.xml
index bd716aae150..6aacf6705b8 100644
--- a/module/finc/tests/phpunit.xml
+++ b/module/finc/tests/phpunit.xml
@@ -6,4 +6,12 @@
         <testsuite name="fincTest">
             <directory>./unit-tests/src</directory>
         </testsuite>
+    <php>
+        <!-- Docker environment variables -->
+        <env name="LOCAL_CACHE_DIR" value="/usr/local/vufind/data/cache"/>
+        <env name="LOCAL_OVERRIDE_DIR" value="/usr/local/vufind/de_15/dev"/>
+        <env name="APPLICATION_PATH" value="/usr/local/vufind"/>
+        <env name="VUFIND_LOCAL_DIR" value="/usr/local/vufind/de_15/dev"/>
+        <env name="VUFIND_ENV" value="development"/>
+    </php>
 </phpunit>
diff --git a/module/finc/tests/unit-tests/src/fincTest/Config/SearchSpecsReaderTest.php b/module/finc/tests/unit-tests/src/fincTest/Config/SearchSpecsReaderTest.php
new file mode 100644
index 00000000000..e172829e6ca
--- /dev/null
+++ b/module/finc/tests/unit-tests/src/fincTest/Config/SearchSpecsReaderTest.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Config SearchSpecsReader Test Class
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2010.
+ * Copyright (C) Leipzig University Library 2020.
+ *
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category Finc
+ * @package  Tests
+ * @author   Robert Lange <lange@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace fincTest\Config;
+
+use Symfony\Component\Yaml\Yaml;
+use VuFind\Config\Locator;
+use VuFind\Config\SearchSpecsReader;
+
+/**
+ * Config SearchSpecsReader Test Class
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Robert Lange <lange@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+class SearchSpecsReaderTest extends \VuFindTest\Config\SearchSpecsReaderTest
+{
+    /**
+     * Name of default yaml name to test.
+     *
+     * @var string
+     */
+    protected static $defaultYamlName = 'searchspecs.yaml';
+
+    /**
+     * YamlReader for SearchSpecs files.
+     *
+     * @var SearchSpecsReader
+     */
+    protected static $reader;
+
+    /**
+     * Path to symlinks and expected searchspecs result.
+     *
+     * @var string
+     */
+    protected $basePathToFixtures;
+
+    /**
+     * Path to local logging / printing.
+     *
+     * @var string
+     */
+    protected $basePathToLogging;
+
+    /**
+     * Override standard setup method.
+     *
+     * @return void
+     */
+    public static function setUpBeforeClass()
+    {
+        // Don't create test files, use existing.
+
+        // CAUTION, DONT DELETE REAL CONFIG FILES!
+        self::$filesToDelete = [];
+        self::$reader = new SearchSpecsReader();
+    }
+
+    /**
+     * Test @parent_yaml directive.
+     *
+     * @return void
+     */
+    public function testParentYaml()
+    {
+        if (self::$writeFailed) {
+            $this->markTestSkipped('Could not write test configurations.');
+        }
+        $this->basePathToFixtures = __DIR__ . "/../../../../fixtures/configs/yaml/searchspecs";
+        $this->basePathToLogging  = __DIR__ . "/log/";
+
+        /* check live finc searchspecs.yaml is equal to old /desired live searchspecs.yaml */
+        $this->isEcpectedSpec('local');
+
+        /* check alpha finc searchspecs.yaml is equal to old /desired alpha searchspecs.yaml */
+        $this->isEcpectedSpec('local/alpha');
+    }
+
+    /**
+     * @param string $environment
+     * @param bool $printResult
+     * @throws \ReflectionException
+     */
+    protected function isEcpectedSpec(string $environment, bool $printResult = false): void
+    {
+        $basePathAppendix   = "config/vufind/" . self::$defaultYamlName;
+
+        $vufindYaml     = "$this->basePathToFixtures/$basePathAppendix";
+        $currentEnvYaml = "$this->basePathToFixtures/$environment/$basePathAppendix";
+        $expectedYaml   = "$this->basePathToFixtures/result/$environment/" . self::$defaultYamlName;
+
+        $resultArray    = $this->callMethod(self::$reader, 'getFromPaths', [$vufindYaml, $currentEnvYaml]);
+        $expectedArray  = $this->callMethod(self::$reader, 'getFromPaths', [$expectedYaml]);
+
+        if ($printResult) {
+            file_put_contents("$this->basePathToLogging/input_reverse.yaml", Yaml::dump($resultArray, 4, 2));
+        }
+
+        $this->assertEquals(
+            $resultArray,
+            $expectedArray
+        );
+    }
+}
-- 
GitLab