[open-ils-commits] [GIT] Evergreen ILS branch master updated. f21505025bfed5e7b6ac94e0c9440d027f327cf9

Evergreen Git git at git.evergreen-ils.org
Mon Aug 28 11:22:54 EDT 2017


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  f21505025bfed5e7b6ac94e0c9440d027f327cf9 (commit)
       via  24cd717e04d3fb4f96d8989c2883d3605f380a79 (commit)
       via  58fe62d8a43aac350830e17f35f80d6d781ce8a0 (commit)
       via  c3a96ff241ee9cb6573cb8d0848992491bad595a (commit)
       via  aaf0056d49bd36518fff992fbbb2d3469c2e1d62 (commit)
       via  3af40daa339a695f11cbeddd2c6e0aeb4ed2ede7 (commit)
       via  95e6be5c2710f47bd60a9e6cda45796a5b83e65e (commit)
       via  19953de64b9e24aaef8f815c356f34b9407d713d (commit)
       via  7319e93ca989f2abccd15b1aff70ab5904aa3cab (commit)
       via  abdf6f8ebc385996be871594af8985a2fc07b4a2 (commit)
       via  ec22696f691b30a085e6f6e4d0ed95de7e63d42e (commit)
       via  688c2a26d0921ba06dd633835348bdda3989591b (commit)
       via  d9fa69dee18b43d5a2efc460411c45f4064ae7cc (commit)
      from  e3fd9e6693dd3beec3711c9dcdd1685a91151cdb (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit f21505025bfed5e7b6ac94e0c9440d027f327cf9
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Mon Aug 28 11:21:16 2017 -0400

    LP#1698206: Stamping upgrade script for copy_vis_attr_cache
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 9cc66db..51fe915 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -90,7 +90,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1056', :eg_version); -- miker/gmcharlt
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1057', :eg_version); -- miker/gmcharlt/kmlussier
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/1057.schema.copy_vis_attr_cache.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
rename to Open-ILS/src/sql/Pg/upgrade/1057.schema.copy_vis_attr_cache.sql
index a6d6065..a2497b8 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1057.schema.copy_vis_attr_cache.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1057', :eg_version); -- miker/gmcharlt/kmlussier
+
 -- Thist change drops a needless join and saves 10-15% in time cost
 CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes text[], hits bigint[]) RETURNS TABLE(id integer, value text, count bigint)
 AS $f$

commit 24cd717e04d3fb4f96d8989c2883d3605f380a79
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Aug 17 18:22:37 2017 -0400

    LP#1698206: basic release notes entry
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Architecture/pure_sql_searching.adoc b/docs/RELEASE_NOTES_NEXT/Architecture/pure_sql_searching.adoc
new file mode 100644
index 0000000..1d8ec6b
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Architecture/pure_sql_searching.adoc
@@ -0,0 +1,4 @@
+Pure-SQL catalog searching
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+Public and staff catalog search is now both more accurate and faster
+by redesigning how the visibility of records is calculated.

commit 58fe62d8a43aac350830e17f35f80d6d781ce8a0
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Aug 17 18:16:39 2017 -0400

    LP#1698206: remove now-superfluous include of List::MoreUtils
    
    I've verified during rebasing that the rework introduced by the
    eliminate staged search branch happen to include an independent
    fix of LP#1624443.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
index 9bed9cd..c788761 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -3,7 +3,6 @@ use strict; use warnings;
 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
 use File::Spec;
 use Time::HiRes qw/time sleep/;
-use List::MoreUtils qw/uniq/;
 use OpenSRF::Utils::Cache;
 use OpenSRF::Utils::Logger qw/$logger/;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;

commit c3a96ff241ee9cb6573cb8d0848992491bad595a
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Aug 17 12:35:03 2017 -0400

    LP#1698206: fix sequence error in schema update script
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
index 041685f..a6d6065 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
@@ -85,202 +85,6 @@ BEGIN
 END;
 $F$ LANGUAGE PLPGSQL STABLE;
 
-CREATE OR REPLACE FUNCTION unapi.mmr_mra (
-    obj_id BIGINT,
-    format TEXT,
-    ename TEXT,
-    includes TEXT[],
-    org TEXT,
-    depth INT DEFAULT NULL,
-    slimit HSTORE DEFAULT NULL,
-    soffset HSTORE DEFAULT NULL,
-    include_xmlns BOOL DEFAULT TRUE,
-    pref_lib INT DEFAULT NULL
-) RETURNS XML AS $F$
-    SELECT  XMLELEMENT(
-        name attributes,
-        XMLATTRIBUTES(
-            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
-            'tag:open-ils.org:U2 at mmr/' || $1 AS metarecord
-        ),
-        (SELECT XMLAGG(foo.y)
-          FROM (
-            WITH sourcelist AS (
-                WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
-                     basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
-                     circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
-                                  FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
-                SELECT  source
-                  FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
-                  WHERE mmsm.metarecord = $1 AND (
-                    EXISTS (
-                        SELECT  1
-                          FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
-                          WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
-                                AND acvac.record = mmsm.source
-                    )
-                    OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
-                    OR EXISTS (SELECT 1 FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = mmsm.source)
-                )
-            )
-            SELECT  cmra.aid,
-                    XMLELEMENT(
-                        name field,
-                        XMLATTRIBUTES(
-                            cmra.attr AS name,
-                            cmra.value AS "coded-value",
-                            cmra.aid AS "cvmid",
-                            rad.composite,
-                            rad.multi,
-                            rad.filter,
-                            rad.sorter,
-                            cmra.source_list
-                        ),
-                        cmra.value
-                    )
-              FROM  (
-                SELECT DISTINCT aid, attr, value, STRING_AGG(x.id::TEXT, ',') AS source_list
-                  FROM (
-                    SELECT  v.source AS id,
-                            c.id AS aid,
-                            c.ctype AS attr,
-                            c.code AS value
-                      FROM  metabib.record_attr_vector_list v
-                            JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) )
-                    ) AS x
-                    JOIN sourcelist ON (x.id = sourcelist.source)
-                    GROUP BY 1, 2, 3
-                ) AS cmra
-                JOIN config.record_attr_definition rad ON (cmra.attr = rad.name)
-                UNION ALL
-            SELECT  umra.aid,
-                    XMLELEMENT(
-                        name field,
-                        XMLATTRIBUTES(
-                            umra.attr AS name,
-                            rad.composite,
-                            rad.multi,
-                            rad.filter,
-                            rad.sorter
-                        ),
-                        umra.value
-                    )
-              FROM  (
-                SELECT DISTINCT aid, attr, value
-                  FROM (
-                    SELECT  v.source AS id,
-                            m.id AS aid,
-                            m.attr AS attr,
-                            m.value AS value
-                      FROM  metabib.record_attr_vector_list v
-                            JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
-                    ) AS x
-                    JOIN sourcelist ON (x.id = sourcelist.source)
-                ) AS umra
-                JOIN config.record_attr_definition rad ON (umra.attr = rad.name)
-                ORDER BY 1
-
-            )foo(id,y)
-        )
-    )
-$F$ LANGUAGE SQL STABLE;
-
-CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
-    bibid BIGINT[],
-    ouid INT,
-    depth INT DEFAULT NULL,
-    slimit HSTORE DEFAULT NULL,
-    soffset HSTORE DEFAULT NULL,
-    pref_lib INT DEFAULT NULL,
-    includes TEXT[] DEFAULT NULL::TEXT[]
-) RETURNS TABLE(id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
-    WITH RECURSIVE ou_depth AS (
-        SELECT COALESCE(
-            $3,
-            (
-                SELECT depth
-                FROM actor.org_unit_type aout
-                    INNER JOIN actor.org_unit ou ON ou_type = aout.id
-                WHERE ou.id = $2
-            )
-        ) AS depth
-    ), descendant_depth AS (
-        SELECT  ou.id,
-                ou.parent_ou,
-                out.depth
-        FROM  actor.org_unit ou
-                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
-                JOIN anscestor_depth ad ON (ad.id = ou.id),
-                ou_depth
-        WHERE ad.depth = ou_depth.depth
-            UNION ALL
-        SELECT  ou.id,
-                ou.parent_ou,
-                out.depth
-        FROM  actor.org_unit ou
-                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
-                JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
-    ), anscestor_depth AS (
-        SELECT  ou.id,
-                ou.parent_ou,
-                out.depth
-        FROM  actor.org_unit ou
-                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
-        WHERE ou.id = $2
-            UNION ALL
-        SELECT  ou.id,
-                ou.parent_ou,
-                out.depth
-        FROM  actor.org_unit ou
-                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
-                JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
-    ), descendants as (
-        SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id)
-    )
-
-    SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
-        SELECT acn.id, owning_lib.name, acn.label_sortkey,
-            evergreen.rank_cp(acp),
-            RANK() OVER w
-        FROM asset.call_number acn
-            JOIN asset.copy acp ON (acn.id = acp.call_number)
-            JOIN descendants AS aou ON (acp.circ_lib = aou.id)
-            JOIN actor.org_unit AS owning_lib ON (acn.owning_lib = owning_lib.id)
-        WHERE acn.record = ANY ($1)
-            AND acn.deleted IS FALSE
-            AND acp.deleted IS FALSE
-            AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
-                EXISTS (
-                    WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
-                         circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
-                    SELECT  1
-                      FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
-                      WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
-                            AND acvac.target_copy = acp.id
-                            AND acvac.record = acn.record
-                ) ELSE TRUE END
-        GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
-        WINDOW w AS (
-            ORDER BY
-                COALESCE(
-                    CASE WHEN aou.id = $2 THEN -20000 END,
-                    CASE WHEN aou.id = $6 THEN -10000 END,
-                    (SELECT distance - 5000
-                        FROM actor.org_unit_descendants_distance($6) as x
-                        WHERE x.id = aou.id AND $6 IN (
-                            SELECT q.id FROM actor.org_unit_descendants($2) as q)),
-                    (SELECT e.distance FROM actor.org_unit_descendants_distance($2) as e WHERE e.id = aou.id),
-                    1000
-                ),
-                evergreen.rank_cp(acp)
-        )
-    ) AS ua
-    GROUP BY ua.id, ua.name, ua.label_sortkey
-    ORDER BY rank, ua.name, ua.label_sortkey
-    LIMIT ($4 -> 'acn')::INT
-    OFFSET ($5 -> 'acn')::INT;
-$$ LANGUAGE SQL STABLE ROWS 10;
-
 CREATE TABLE asset.copy_vis_attr_cache (
     id              BIGSERIAL   PRIMARY KEY,
     record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
@@ -1208,5 +1012,201 @@ BEGIN
 END;
 $f$ LANGUAGE PLPGSQL;
 
+CREATE OR REPLACE FUNCTION unapi.mmr_mra (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+        name attributes,
+        XMLATTRIBUTES(
+            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+            'tag:open-ils.org:U2 at mmr/' || $1 AS metarecord
+        ),
+        (SELECT XMLAGG(foo.y)
+          FROM (
+            WITH sourcelist AS (
+                WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
+                     basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
+                     circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
+                                  FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
+                SELECT  source
+                  FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
+                  WHERE mmsm.metarecord = $1 AND (
+                    EXISTS (
+                        SELECT  1
+                          FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
+                          WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
+                                AND acvac.record = mmsm.source
+                    )
+                    OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
+                    OR EXISTS (SELECT 1 FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = mmsm.source)
+                )
+            )
+            SELECT  cmra.aid,
+                    XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            cmra.attr AS name,
+                            cmra.value AS "coded-value",
+                            cmra.aid AS "cvmid",
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter,
+                            cmra.source_list
+                        ),
+                        cmra.value
+                    )
+              FROM  (
+                SELECT DISTINCT aid, attr, value, STRING_AGG(x.id::TEXT, ',') AS source_list
+                  FROM (
+                    SELECT  v.source AS id,
+                            c.id AS aid,
+                            c.ctype AS attr,
+                            c.code AS value
+                      FROM  metabib.record_attr_vector_list v
+                            JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) )
+                    ) AS x
+                    JOIN sourcelist ON (x.id = sourcelist.source)
+                    GROUP BY 1, 2, 3
+                ) AS cmra
+                JOIN config.record_attr_definition rad ON (cmra.attr = rad.name)
+                UNION ALL
+            SELECT  umra.aid,
+                    XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            umra.attr AS name,
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter
+                        ),
+                        umra.value
+                    )
+              FROM  (
+                SELECT DISTINCT aid, attr, value
+                  FROM (
+                    SELECT  v.source AS id,
+                            m.id AS aid,
+                            m.attr AS attr,
+                            m.value AS value
+                      FROM  metabib.record_attr_vector_list v
+                            JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
+                    ) AS x
+                    JOIN sourcelist ON (x.id = sourcelist.source)
+                ) AS umra
+                JOIN config.record_attr_definition rad ON (umra.attr = rad.name)
+                ORDER BY 1
+
+            )foo(id,y)
+        )
+    )
+$F$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
+    bibid BIGINT[],
+    ouid INT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    pref_lib INT DEFAULT NULL,
+    includes TEXT[] DEFAULT NULL::TEXT[]
+) RETURNS TABLE(id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
+    WITH RECURSIVE ou_depth AS (
+        SELECT COALESCE(
+            $3,
+            (
+                SELECT depth
+                FROM actor.org_unit_type aout
+                    INNER JOIN actor.org_unit ou ON ou_type = aout.id
+                WHERE ou.id = $2
+            )
+        ) AS depth
+    ), descendant_depth AS (
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                JOIN anscestor_depth ad ON (ad.id = ou.id),
+                ou_depth
+        WHERE ad.depth = ou_depth.depth
+            UNION ALL
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
+    ), anscestor_depth AS (
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+        WHERE ou.id = $2
+            UNION ALL
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
+    ), descendants as (
+        SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id)
+    )
+
+    SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
+        SELECT acn.id, owning_lib.name, acn.label_sortkey,
+            evergreen.rank_cp(acp),
+            RANK() OVER w
+        FROM asset.call_number acn
+            JOIN asset.copy acp ON (acn.id = acp.call_number)
+            JOIN descendants AS aou ON (acp.circ_lib = aou.id)
+            JOIN actor.org_unit AS owning_lib ON (acn.owning_lib = owning_lib.id)
+        WHERE acn.record = ANY ($1)
+            AND acn.deleted IS FALSE
+            AND acp.deleted IS FALSE
+            AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
+                EXISTS (
+                    WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
+                         circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
+                    SELECT  1
+                      FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
+                      WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
+                            AND acvac.target_copy = acp.id
+                            AND acvac.record = acn.record
+                ) ELSE TRUE END
+        GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
+        WINDOW w AS (
+            ORDER BY
+                COALESCE(
+                    CASE WHEN aou.id = $2 THEN -20000 END,
+                    CASE WHEN aou.id = $6 THEN -10000 END,
+                    (SELECT distance - 5000
+                        FROM actor.org_unit_descendants_distance($6) as x
+                        WHERE x.id = aou.id AND $6 IN (
+                            SELECT q.id FROM actor.org_unit_descendants($2) as q)),
+                    (SELECT e.distance FROM actor.org_unit_descendants_distance($2) as e WHERE e.id = aou.id),
+                    1000
+                ),
+                evergreen.rank_cp(acp)
+        )
+    ) AS ua
+    GROUP BY ua.id, ua.name, ua.label_sortkey
+    ORDER BY rank, ua.name, ua.label_sortkey
+    LIMIT ($4 -> 'acn')::INT
+    OFFSET ($5 -> 'acn')::INT;
+$$ LANGUAGE SQL STABLE ROWS 10;
+
 COMMIT;
 

commit aaf0056d49bd36518fff992fbbb2d3469c2e1d62
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Aug 9 14:14:42 2017 -0400

    LP#1698206: Remove remaining SQL uses of the old copy visibility cache
    
    There were two remaining uses of the old copy vis cache in SQL functions used
    to render OPAC pages.  This commit gets rid of those.
    
    There is still one reference to the asset.opac_visible_copies table in the old
    staged-search function, but that is not used anywhere in the code now, so no
    need to change that.  Instead, we should start pruning old code.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/990.schema.unapi.sql b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
index 617946a..4a8c3ac 100644
--- a/Open-ILS/src/sql/Pg/990.schema.unapi.sql
+++ b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
@@ -152,9 +152,13 @@ CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
             AND acp.deleted IS FALSE
             AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN 
                 EXISTS (
-                    SELECT 1 
-                    FROM asset.opac_visible_copies 
-                    WHERE copy_id = acp.id AND record = acn.record
+                    WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
+                         circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
+                    SELECT  1 
+                      FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
+                      WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
+                            AND acvac.target_copy = acp.id
+                            AND acvac.record = acn.record
                 ) ELSE TRUE END
         GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
         WINDOW w AS (
@@ -1424,19 +1428,21 @@ CREATE OR REPLACE FUNCTION unapi.mmr_mra (
         (SELECT XMLAGG(foo.y)
           FROM (
             WITH sourcelist AS (
-                WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id
-                    FROM actor.org_unit WHERE shortname = $5 LIMIT 1)
-                SELECT source
-                FROM metabib.metarecord_source_map mmsm, aou
-                WHERE metarecord = $1 AND (
+                WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
+                     basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
+                     circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
+                                  FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
+                SELECT  source
+                  FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
+                  WHERE mmsm.metarecord = $1 AND (
                     EXISTS (
-                        SELECT 1 FROM asset.opac_visible_copies
-                        WHERE record = source AND circ_lib IN (
-                            SELECT id FROM actor.org_unit_descendants(aou.id, $6))
-                        LIMIT 1
+                        SELECT  1
+                          FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
+                          WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
+                                AND acvac.record = mmsm.source
                     )
-                    OR EXISTS (SELECT 1 FROM located_uris(source, aou.id, $10) LIMIT 1)
-                    OR EXISTS (SELECT 1 FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = mmsm.source LIMIT 1)
+                    OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
+                    OR EXISTS (SELECT 1 FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = mmsm.source)
                 )
             )
             SELECT  cmra.aid,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
index 6afae2a..041685f 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
@@ -85,6 +85,202 @@ BEGIN
 END;
 $F$ LANGUAGE PLPGSQL STABLE;
 
+CREATE OR REPLACE FUNCTION unapi.mmr_mra (
+    obj_id BIGINT,
+    format TEXT,
+    ename TEXT,
+    includes TEXT[],
+    org TEXT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    include_xmlns BOOL DEFAULT TRUE,
+    pref_lib INT DEFAULT NULL
+) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+        name attributes,
+        XMLATTRIBUTES(
+            CASE WHEN $9 THEN 'http://open-ils.org/spec/indexing/v1' ELSE NULL END AS xmlns,
+            'tag:open-ils.org:U2 at mmr/' || $1 AS metarecord
+        ),
+        (SELECT XMLAGG(foo.y)
+          FROM (
+            WITH sourcelist AS (
+                WITH aou AS (SELECT COALESCE(id, (evergreen.org_top()).id) AS id FROM actor.org_unit WHERE shortname = $5 LIMIT 1),
+                     basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
+                     circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY_AGG(aoud.id)) AS mask
+                                  FROM aou, LATERAL actor.org_unit_descendants(aou.id, $6) aoud)
+                SELECT  source
+                  FROM  aou, circvm, basevm, metabib.metarecord_source_map mmsm
+                  WHERE mmsm.metarecord = $1 AND (
+                    EXISTS (
+                        SELECT  1
+                          FROM  circvm, basevm, asset.copy_vis_attr_cache acvac
+                          WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
+                                AND acvac.record = mmsm.source
+                    )
+                    OR EXISTS (SELECT 1 FROM evergreen.located_uris(source, aou.id, $10) LIMIT 1)
+                    OR EXISTS (SELECT 1 FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = mmsm.source)
+                )
+            )
+            SELECT  cmra.aid,
+                    XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            cmra.attr AS name,
+                            cmra.value AS "coded-value",
+                            cmra.aid AS "cvmid",
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter,
+                            cmra.source_list
+                        ),
+                        cmra.value
+                    )
+              FROM  (
+                SELECT DISTINCT aid, attr, value, STRING_AGG(x.id::TEXT, ',') AS source_list
+                  FROM (
+                    SELECT  v.source AS id,
+                            c.id AS aid,
+                            c.ctype AS attr,
+                            c.code AS value
+                      FROM  metabib.record_attr_vector_list v
+                            JOIN config.coded_value_map c ON ( c.id = ANY( v.vlist ) )
+                    ) AS x
+                    JOIN sourcelist ON (x.id = sourcelist.source)
+                    GROUP BY 1, 2, 3
+                ) AS cmra
+                JOIN config.record_attr_definition rad ON (cmra.attr = rad.name)
+                UNION ALL
+            SELECT  umra.aid,
+                    XMLELEMENT(
+                        name field,
+                        XMLATTRIBUTES(
+                            umra.attr AS name,
+                            rad.composite,
+                            rad.multi,
+                            rad.filter,
+                            rad.sorter
+                        ),
+                        umra.value
+                    )
+              FROM  (
+                SELECT DISTINCT aid, attr, value
+                  FROM (
+                    SELECT  v.source AS id,
+                            m.id AS aid,
+                            m.attr AS attr,
+                            m.value AS value
+                      FROM  metabib.record_attr_vector_list v
+                            JOIN metabib.uncontrolled_record_attr_value m ON ( m.id = ANY( v.vlist ) )
+                    ) AS x
+                    JOIN sourcelist ON (x.id = sourcelist.source)
+                ) AS umra
+                JOIN config.record_attr_definition rad ON (umra.attr = rad.name)
+                ORDER BY 1
+
+            )foo(id,y)
+        )
+    )
+$F$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION evergreen.ranked_volumes(
+    bibid BIGINT[],
+    ouid INT,
+    depth INT DEFAULT NULL,
+    slimit HSTORE DEFAULT NULL,
+    soffset HSTORE DEFAULT NULL,
+    pref_lib INT DEFAULT NULL,
+    includes TEXT[] DEFAULT NULL::TEXT[]
+) RETURNS TABLE(id BIGINT, name TEXT, label_sortkey TEXT, rank BIGINT) AS $$
+    WITH RECURSIVE ou_depth AS (
+        SELECT COALESCE(
+            $3,
+            (
+                SELECT depth
+                FROM actor.org_unit_type aout
+                    INNER JOIN actor.org_unit ou ON ou_type = aout.id
+                WHERE ou.id = $2
+            )
+        ) AS depth
+    ), descendant_depth AS (
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                JOIN anscestor_depth ad ON (ad.id = ou.id),
+                ou_depth
+        WHERE ad.depth = ou_depth.depth
+            UNION ALL
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
+    ), anscestor_depth AS (
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+        WHERE ou.id = $2
+            UNION ALL
+        SELECT  ou.id,
+                ou.parent_ou,
+                out.depth
+        FROM  actor.org_unit ou
+                JOIN actor.org_unit_type out ON (out.id = ou.ou_type)
+                JOIN anscestor_depth ot ON (ot.parent_ou = ou.id)
+    ), descendants as (
+        SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id)
+    )
+
+    SELECT ua.id, ua.name, ua.label_sortkey, MIN(ua.rank) AS rank FROM (
+        SELECT acn.id, owning_lib.name, acn.label_sortkey,
+            evergreen.rank_cp(acp),
+            RANK() OVER w
+        FROM asset.call_number acn
+            JOIN asset.copy acp ON (acn.id = acp.call_number)
+            JOIN descendants AS aou ON (acp.circ_lib = aou.id)
+            JOIN actor.org_unit AS owning_lib ON (acn.owning_lib = owning_lib.id)
+        WHERE acn.record = ANY ($1)
+            AND acn.deleted IS FALSE
+            AND acp.deleted IS FALSE
+            AND CASE WHEN ('exclude_invisible_acn' = ANY($7)) THEN
+                EXISTS (
+                    WITH basevm AS (SELECT c_attrs FROM  asset.patron_default_visibility_mask()),
+                         circvm AS (SELECT search.calculate_visibility_attribute_test('circ_lib', ARRAY[acp.circ_lib]) AS mask)
+                    SELECT  1
+                      FROM  basevm, circvm, asset.copy_vis_attr_cache acvac
+                      WHERE acvac.vis_attr_vector @@ (basevm.c_attrs || '&' || circvm.mask)::query_int
+                            AND acvac.target_copy = acp.id
+                            AND acvac.record = acn.record
+                ) ELSE TRUE END
+        GROUP BY acn.id, evergreen.rank_cp(acp), owning_lib.name, acn.label_sortkey, aou.id
+        WINDOW w AS (
+            ORDER BY
+                COALESCE(
+                    CASE WHEN aou.id = $2 THEN -20000 END,
+                    CASE WHEN aou.id = $6 THEN -10000 END,
+                    (SELECT distance - 5000
+                        FROM actor.org_unit_descendants_distance($6) as x
+                        WHERE x.id = aou.id AND $6 IN (
+                            SELECT q.id FROM actor.org_unit_descendants($2) as q)),
+                    (SELECT e.distance FROM actor.org_unit_descendants_distance($2) as e WHERE e.id = aou.id),
+                    1000
+                ),
+                evergreen.rank_cp(acp)
+        )
+    ) AS ua
+    GROUP BY ua.id, ua.name, ua.label_sortkey
+    ORDER BY rank, ua.name, ua.label_sortkey
+    LIMIT ($4 -> 'acn')::INT
+    OFFSET ($5 -> 'acn')::INT;
+$$ LANGUAGE SQL STABLE ROWS 10;
+
 CREATE TABLE asset.copy_vis_attr_cache (
     id              BIGSERIAL   PRIMARY KEY,
     record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.

commit 3af40daa339a695f11cbeddd2c6e0aeb4ed2ede7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 4 13:22:24 2017 -0400

    LP#1698206: Remove remaining uses of the old copy visibility cache
    
    Some Perl was still using the old cache table, so this teaches them the new
    style.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
index c6118f2..e489e26 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
@@ -1052,10 +1052,17 @@ sub MR_records_matching_format {
     if ($opac_visible) {
         $vis_q = <<'        SQL';
             EXISTS(
-                SELECT 1 FROM asset.opac_visible_copies
-                WHERE record = ? AND circ_lib IN (
-                    SELECT id FROM actor.org_unit_descendants(?)
-                )
+                SELECT  1
+                  FROM  asset.patron_default_visibility_mask() mask,
+                        asset.copy_vis_attr_cache v
+                        JOIN asset.copy c ON (
+                            c.id = v.target_copy
+                            AND v.record = ?
+                            AND c.circ_lib IN (
+                                SELECT id FROM actor.org_unit_descendants(?)
+                            )
+                        )
+                  WHERE v.vis_attr_vector @@ mask.c_attrs::query_int
             )
         SQL
     }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
index aacd6b2..a8b2161 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
@@ -95,7 +95,8 @@ sub ordered_records_from_metarecord { # XXX Replace with QP-based search-within-
     my $org = shift;
     my $depth = shift;
 
-    my $copies_visible = 'LEFT JOIN asset.opac_visible_copies vc ON (br.id = vc.record)';
+    my $copies_visible = 'LEFT JOIN asset.copy_attr_vis_cache vc ON (br.id = vc.record '.
+                         'AND vc.vis_attr_vector @@ (SELECT c_attrs::query_int FROM asset.patron_default_visibility_mask() LIMIT 1))';
     $copies_visible = '' if ($self->api_name =~ /staff/o);
 
     my $copies_visible_count = ',COUNT(vc.id)';
diff --git a/Open-ILS/src/support-scripts/sitemap_generator b/Open-ILS/src/support-scripts/sitemap_generator
index 5c15723..16fe5fd 100755
--- a/Open-ILS/src/support-scripts/sitemap_generator
+++ b/Open-ILS/src/support-scripts/sitemap_generator
@@ -154,7 +154,13 @@ sub get_record_ids {
                     ELSE bre.edit_date::date
                 END AS edit_date
             FROM biblio.record_entry bre
-                INNER JOIN asset.opac_visible_copies aovc ON bre.id = aovc.record
+                 INNER JOIN asset.copy_attr_vis_cache vc ON (bre.id = vc.record
+                     AND vc.vis_attr_vector @@ (
+                         SELECT  c_attrs::query_int
+                           FROM  asset.patron_default_visibility_mask()
+                           LIMIT 1
+                    )
+                 )
     ";
     if ($aou_id) {
         $q .= " WHERE circ_lib IN (SELECT id FROM copy_orgs)";

commit 95e6be5c2710f47bd60a9e6cda45796a5b83e65e
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 4 12:52:05 2017 -0400

    LP#1698206: Copy counts generated from new vis cache data
    
    The unAPI, erm, API was depending on old copy visibility caching tables.  Here
    we teach it to use the new style.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index 364702e..0faba5f 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -554,16 +554,21 @@ BEGIN
 
     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
         RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
         SELECT  ans.depth,
                 ans.id,
                 COUNT( av.id ),
-                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
                 COUNT( av.id ),
                 trans
-          FROM  
-                actor.org_unit_descendants(ans.id) d
-                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          FROM  mask,
+                available_statuses,
+                org_list,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -585,16 +590,20 @@ BEGIN
 
     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
         RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
         SELECT  -1,
                 ans.id,
                 COUNT( av.id ),
-                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
                 COUNT( av.id ),
                 trans
-          FROM
-                actor.org_unit_descendants(ans.id) d
-                JOIN asset.opac_visible_copies av ON (av.record = rid AND av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          FROM  mask,
+                org_list,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -724,17 +733,22 @@ BEGIN
 
     FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
         RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
         SELECT  ans.depth,
                 ans.id,
                 COUNT( av.id ),
-                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
                 COUNT( av.id ),
                 trans
-          FROM  
-                actor.org_unit_descendants(ans.id) d
-                JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          FROM  mask,
+                org_list,
+                available_statuses,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy)
                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
@@ -756,17 +770,22 @@ BEGIN
 
     FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
         RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
         SELECT  -1,
                 ans.id,
                 COUNT( av.id ),
-                SUM( CASE WHEN cp.status IN (0,7,12) THEN 1 ELSE 0 END ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
                 COUNT( av.id ),
                 trans
-          FROM
-                actor.org_unit_descendants(ans.id) d
-                JOIN asset.opac_visible_copies av ON (av.circ_lib = d.id)
-                JOIN asset.copy cp ON (cp.id = av.copy_id)
+          FROM  mask,
+                org_list,
+                available_statuses,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy)
                 JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
           GROUP BY 1,2,6;
 
         IF NOT FOUND THEN
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
index e2e1fcd..6afae2a 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
@@ -867,5 +867,150 @@ CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON serial.unit FOR EACH
 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
 CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
 
+CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+                COUNT( av.id ),
+                trans
+          FROM  mask,
+                available_statuses,
+                org_list,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+                COUNT( av.id ),
+                trans
+          FROM  mask,
+                org_list,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_ou_metarecord_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
+
+    FOR ans IN SELECT u.id, t.depth FROM actor.org_unit_ancestors(org) AS u JOIN actor.org_unit_type t ON (u.ou_type = t.id) LOOP
+        RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+        SELECT  ans.depth,
+                ans.id,
+                COUNT( av.id ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+                COUNT( av.id ),
+                trans
+          FROM  mask,
+                org_list,
+                available_statuses,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+    ans RECORD;
+    trans INT;
+BEGIN
+    SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) JOIN metabib.metarecord_source_map m ON (m.source = b.id) WHERE src.transcendant AND m.metarecord = rid;
+
+    FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+        RETURN QUERY
+        WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+             available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+             mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+        SELECT  -1,
+                ans.id,
+                COUNT( av.id ),
+                SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+                COUNT( av.id ),
+                trans
+          FROM  mask,
+                org_list,
+                available_statuses,
+                asset.copy_vis_attr_cache av
+                JOIN asset.copy cp ON (cp.id = av.target_copy)
+                JOIN metabib.metarecord_source_map m ON (m.metarecord = rid AND m.source = av.record)
+          WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+          GROUP BY 1,2,6;
+
+        IF NOT FOUND THEN
+            RETURN QUERY SELECT ans.depth, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+        END IF;
+
+    END LOOP;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
 COMMIT;
 

commit 19953de64b9e24aaef8f815c356f34b9407d713d
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Jul 31 11:40:07 2017 -0400

    LP#1698206: Reify baseline schema
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/010.schema.biblio.sql b/Open-ILS/src/sql/Pg/010.schema.biblio.sql
index f0c0133..53ddea5 100644
--- a/Open-ILS/src/sql/Pg/010.schema.biblio.sql
+++ b/Open-ILS/src/sql/Pg/010.schema.biblio.sql
@@ -52,6 +52,7 @@ CREATE TABLE biblio.record_entry (
 	tcn_value	TEXT		NOT NULL DEFAULT biblio.next_autogen_tcn_value(),
 	marc		TEXT		NOT NULL,
 	last_xact_id	TEXT		NOT NULL,
+    vis_attr_vector INT[],
     owner       INT,
     share_depth INT
 );
diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index fb9a83f..0d0712e 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -1792,146 +1792,8 @@ BEGIN
 END;
 $$ LANGUAGE PLPGSQL;
 
-
-CREATE OR REPLACE
-    FUNCTION metabib.suggest_browse_entries(
-        raw_query_text  TEXT,   -- actually typed by humans at the UI level
-        search_class    TEXT,   -- 'alias' or 'class' or 'class|field..', etc
-        headline_opts   TEXT,   -- markup options for ts_headline()
-        visibility_org  INTEGER,-- null if you don't want opac visibility test
-        query_limit     INTEGER,-- use in LIMIT clause of interal query
-        normalization   INTEGER -- argument to TS_RANK_CD()
-    ) RETURNS TABLE (
-        value                   TEXT,   -- plain
-        field                   INTEGER,
-        buoyant_and_class_match BOOL,
-        field_match             BOOL,
-        field_weight            INTEGER,
-        rank                    REAL,
-        buoyant                 BOOL,
-        match                   TEXT    -- marked up
-    ) AS $func$
-DECLARE
-    prepared_query_texts    TEXT[];
-    query                   TSQUERY;
-    plain_query             TSQUERY;
-    opac_visibility_join    TEXT;
-    search_class_join       TEXT;
-    r_fields                RECORD;
-BEGIN
-    prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
-
-    query := TO_TSQUERY('keyword', prepared_query_texts[1]);
-    plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
-
-    visibility_org := NULLIF(visibility_org,-1);
-    IF visibility_org IS NOT NULL THEN
-        PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
-        IF FOUND THEN
-            opac_visibility_join := '';
-        ELSE
-            opac_visibility_join := '
-    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
-    JOIN vm ON (acvac.vis_attr_vector @@
-            (vm.c_attrs || $$&$$ ||
-                search.calculate_visibility_attribute_test(
-                    $$circ_lib$$,
-                    (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
-                )
-            )::query_int
-         )
-';
-        END IF;
-    ELSE
-        opac_visibility_join := '';
-    END IF;
-
-    -- The following determines whether we only provide suggestsons matching
-    -- the user's selected search_class, or whether we show other suggestions
-    -- too. The reason for MIN() is that for search_classes like
-    -- 'title|proper|uniform' you would otherwise get multiple rows.  The
-    -- implication is that if title as a class doesn't have restrict,
-    -- nor does the proper field, but the uniform field does, you're going
-    -- to get 'false' for your overall evaluation of 'should we restrict?'
-    -- To invert that, change from MIN() to MAX().
-
-    SELECT
-        INTO r_fields
-            MIN(cmc.restrict::INT) AS restrict_class,
-            MIN(cmf.restrict::INT) AS restrict_field
-        FROM metabib.search_class_to_registered_components(search_class)
-            AS _registered (field_class TEXT, field INT)
-        JOIN
-            config.metabib_class cmc ON (cmc.name = _registered.field_class)
-        LEFT JOIN
-            config.metabib_field cmf ON (cmf.id = _registered.field);
-
-    -- evaluate 'should we restrict?'
-    IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
-        search_class_join := '
-    JOIN
-        metabib.search_class_to_registered_components($2)
-        AS _registered (field_class TEXT, field INT) ON (
-            (_registered.field IS NULL AND
-                _registered.field_class = cmf.field_class) OR
-            (_registered.field = cmf.id)
-        )
-    ';
-    ELSE
-        search_class_join := '
-    LEFT JOIN
-        metabib.search_class_to_registered_components($2)
-        AS _registered (field_class TEXT, field INT) ON (
-            _registered.field_class = cmc.name
-        )
-    ';
-    END IF;
-
-    RETURN QUERY EXECUTE '
-SELECT  DISTINCT
-        x.value,
-        x.id,
-        x.push,
-        x.restrict,
-        x.weight,
-        x.ts_rank_cd,
-        x.buoyant,
-        TS_HEADLINE(value, $7, $3)
-  FROM  (SELECT DISTINCT
-                mbe.value,
-                cmf.id,
-                cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
-                _registered.field = cmf.id AS restrict,
-                cmf.weight,
-                TS_RANK_CD(mbe.index_vector, $1, $6),
-                cmc.buoyant,
-                mbedm.source
-          FROM  metabib.browse_entry_def_map mbedm
-                JOIN (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000) mbe ON (mbe.id = mbedm.entry)
-                JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
-                JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
-                '  || search_class_join || '
-          ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
-          LIMIT 1000) AS x
-        ' || opac_visibility_join || '
-  ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
-  LIMIT $5
-'   -- sic, repeat the order by clause in the outer select too
-    USING
-        query, search_class, headline_opts,
-        visibility_org, query_limit, normalization, plain_query
-        ;
-
-    -- sort order:
-    --  buoyant AND chosen class = match class
-    --  chosen field = match field
-    --  field weight
-    --  rank
-    --  buoyancy
-    --  value itself
-
-END;
-$func$ LANGUAGE PLPGSQL;
+-- Functions metabib.browse, metabib.staged_browse, and metabib.suggest_browse_entries
+-- will be created later, after internal dependencies are resolved.
 
 CREATE OR REPLACE FUNCTION public.oils_tsearch2 () RETURNS TRIGGER AS $$
 DECLARE
@@ -2121,355 +1983,6 @@ CREATE OR REPLACE FUNCTION metabib.browse_pivot(
 $p$ LANGUAGE SQL STABLE;
 
 
-CREATE OR REPLACE FUNCTION metabib.staged_browse(
-    query                   TEXT,
-    fields                  INT[],
-    context_org             INT,
-    context_locations       INT[],
-    staff                   BOOL,
-    browse_superpage_size   INT,
-    count_up_from_zero      BOOL,   -- if false, count down from -1
-    result_limit            INT,
-    next_pivot_pos          INT
-) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
-DECLARE
-    curs                    REFCURSOR;
-    rec                     RECORD;
-    qpfts_query             TEXT;
-    aqpfts_query            TEXT;
-    afields                 INT[];
-    bfields                 INT[];
-    result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
-    results_skipped         INT := 0;
-    row_counter             INT := 0;
-    row_number              INT;
-    slice_start             INT;
-    slice_end               INT;
-    full_end                INT;
-    all_records             BIGINT[];
-    all_brecords             BIGINT[];
-    all_arecords            BIGINT[];
-    superpage_of_records    BIGINT[];
-    superpage_size          INT;
-BEGIN
-    IF count_up_from_zero THEN
-        row_number := 0;
-    ELSE
-        row_number := -1;
-    END IF;
-
-    OPEN curs FOR EXECUTE query;
-
-    LOOP
-        FETCH curs INTO rec;
-        IF NOT FOUND THEN
-            IF result_row.pivot_point IS NOT NULL THEN
-                RETURN NEXT result_row;
-            END IF;
-            RETURN;
-        END IF;
-
-
-        -- Gather aggregate data based on the MBE row we're looking at now, authority axis
-        SELECT INTO all_arecords, result_row.sees, afields
-                ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
-                STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
-                ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
-
-          FROM  metabib.browse_entry_simple_heading_map mbeshm
-                JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
-                JOIN authority.authority_linking aal ON ( ash.record = aal.source )
-                JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
-                JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
-                    ash.atag = map.authority_field
-                    AND map.metabib_field = ANY(fields)
-                )
-          WHERE mbeshm.entry = rec.id;
-
-
-        -- Gather aggregate data based on the MBE row we're looking at now, bib axis
-        SELECT INTO all_brecords, result_row.authorities, bfields
-                ARRAY_AGG(DISTINCT source),
-                STRING_AGG(DISTINCT authority::TEXT, $$,$$),
-                ARRAY_AGG(DISTINCT def)
-          FROM  metabib.browse_entry_def_map
-          WHERE entry = rec.id
-                AND def = ANY(fields);
-
-        SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
-
-        result_row.sources := 0;
-        result_row.asources := 0;
-
-        -- Bib-linked vis checking
-        IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
-
-            full_end := ARRAY_LENGTH(all_brecords, 1);
-            superpage_size := COALESCE(browse_superpage_size, full_end);
-            slice_start := 1;
-            slice_end := superpage_size;
-
-            WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
-                superpage_of_records := all_brecords[slice_start:slice_end];
-                qpfts_query :=
-                    'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
-                    'NULL AS badges, NULL::NUMERIC AS popularity, ' ||
-                    '1::NUMERIC AS rel FROM (SELECT UNNEST(' ||
-                    quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
-
-                -- We use search.query_parser_fts() for visibility testing.
-                -- We're calling it once per browse-superpage worth of records
-                -- out of the set of records related to a given mbe, until we've
-                -- either exhausted that set of records or found at least 1
-                -- visible record.
-
-                SELECT INTO result_row.sources visible
-                    FROM search.query_parser_fts(
-                        context_org, NULL, qpfts_query, NULL,
-                        context_locations, 0, NULL, NULL, FALSE, staff, FALSE
-                    ) qpfts
-                    WHERE qpfts.rel IS NULL;
-
-                slice_start := slice_start + superpage_size;
-                slice_end := slice_end + superpage_size;
-            END LOOP;
-
-            -- Accurate?  Well, probably.
-            result_row.accurate := browse_superpage_size IS NULL OR
-                browse_superpage_size >= full_end;
-
-        END IF;
-
-        -- Authority-linked vis checking
-        IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
-
-            full_end := ARRAY_LENGTH(all_arecords, 1);
-            superpage_size := COALESCE(browse_superpage_size, full_end);
-            slice_start := 1;
-            slice_end := superpage_size;
-
-            WHILE result_row.asources = 0 AND slice_start <= full_end LOOP
-                superpage_of_records := all_arecords[slice_start:slice_end];
-                qpfts_query :=
-                    'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
-                    'NULL AS badges, NULL::NUMERIC AS popularity, ' ||
-                    '1::NUMERIC AS rel FROM (SELECT UNNEST(' ||
-                    quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
-
-                -- We use search.query_parser_fts() for visibility testing.
-                -- We're calling it once per browse-superpage worth of records
-                -- out of the set of records related to a given mbe, via
-                -- authority until we've either exhausted that set of records
-                -- or found at least 1 visible record.
-
-                SELECT INTO result_row.asources visible
-                    FROM search.query_parser_fts(
-                        context_org, NULL, qpfts_query, NULL,
-                        context_locations, 0, NULL, NULL, FALSE, staff, FALSE
-                    ) qpfts
-                    WHERE qpfts.rel IS NULL;
-
-                slice_start := slice_start + superpage_size;
-                slice_end := slice_end + superpage_size;
-            END LOOP;
-
-
-            -- Accurate?  Well, probably.
-            result_row.aaccurate := browse_superpage_size IS NULL OR
-                browse_superpage_size >= full_end;
-
-        END IF;
-
-        IF result_row.sources > 0 OR result_row.asources > 0 THEN
-
-            -- The function that calls this function needs row_number in order
-            -- to correctly order results from two different runs of this
-            -- functions.
-            result_row.row_number := row_number;
-
-            -- Now, if row_counter is still less than limit, return a row.  If
-            -- not, but it is less than next_pivot_pos, continue on without
-            -- returning actual result rows until we find
-            -- that next pivot, and return it.
-
-            IF row_counter < result_limit THEN
-                result_row.browse_entry := rec.id;
-                result_row.value := rec.value;
-
-                RETURN NEXT result_row;
-            ELSE
-                result_row.browse_entry := NULL;
-                result_row.authorities := NULL;
-                result_row.fields := NULL;
-                result_row.value := NULL;
-                result_row.sources := NULL;
-                result_row.sees := NULL;
-                result_row.accurate := NULL;
-                result_row.aaccurate := NULL;
-                result_row.pivot_point := rec.id;
-
-                IF row_counter >= next_pivot_pos THEN
-                    RETURN NEXT result_row;
-                    RETURN;
-                END IF;
-            END IF;
-
-            IF count_up_from_zero THEN
-                row_number := row_number + 1;
-            ELSE
-                row_number := row_number - 1;
-            END IF;
-
-            -- row_counter is different from row_number.
-            -- It simply counts up from zero so that we know when
-            -- we've reached our limit.
-            row_counter := row_counter + 1;
-        END IF;
-    END LOOP;
-END;
-$p$ LANGUAGE PLPGSQL;
-
-
-CREATE OR REPLACE FUNCTION metabib.browse(
-    search_field            INT[],
-    browse_term             TEXT,
-    context_org             INT DEFAULT NULL,
-    context_loc_group       INT DEFAULT NULL,
-    staff                   BOOL DEFAULT FALSE,
-    pivot_id                BIGINT DEFAULT NULL,
-    result_limit            INT DEFAULT 10
-) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
-DECLARE
-    core_query              TEXT;
-    back_query              TEXT;
-    forward_query           TEXT;
-    pivot_sort_value        TEXT;
-    pivot_sort_fallback     TEXT;
-    context_locations       INT[];
-    browse_superpage_size   INT;
-    results_skipped         INT := 0;
-    back_limit              INT;
-    back_to_pivot           INT;
-    forward_limit           INT;
-    forward_to_pivot        INT;
-BEGIN
-    -- First, find the pivot if we were given a browse term but not a pivot.
-    IF pivot_id IS NULL THEN
-        pivot_id := metabib.browse_pivot(search_field, browse_term);
-    END IF;
-
-    SELECT INTO pivot_sort_value, pivot_sort_fallback
-        sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
-
-    -- Bail if we couldn't find a pivot.
-    IF pivot_sort_value IS NULL THEN
-        RETURN;
-    END IF;
-
-    -- Transform the context_loc_group argument (if any) (logc at the
-    -- TPAC layer) into a form we'll be able to use.
-    IF context_loc_group IS NOT NULL THEN
-        SELECT INTO context_locations ARRAY_AGG(location)
-            FROM asset.copy_location_group_map
-            WHERE lgroup = context_loc_group;
-    END IF;
-
-    -- Get the configured size of browse superpages.
-    SELECT INTO browse_superpage_size value     -- NULL ok
-        FROM config.global_flag
-        WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
-
-    -- First we're going to search backward from the pivot, then we're going
-    -- to search forward.  In each direction, we need two limits.  At the
-    -- lesser of the two limits, we delineate the edge of the result set
-    -- we're going to return.  At the greater of the two limits, we find the
-    -- pivot value that would represent an offset from the current pivot
-    -- at a distance of one "page" in either direction, where a "page" is a
-    -- result set of the size specified in the "result_limit" argument.
-    --
-    -- The two limits in each direction make four derived values in total,
-    -- and we calculate them now.
-    back_limit := CEIL(result_limit::FLOAT / 2);
-    back_to_pivot := result_limit;
-    forward_limit := result_limit / 2;
-    forward_to_pivot := result_limit - 1;
-
-    -- This is the meat of the SQL query that finds browse entries.  We'll
-    -- pass this to a function which uses it with a cursor, so that individual
-    -- rows may be fetched in a loop until some condition is satisfied, without
-    -- waiting for a result set of fixed size to be collected all at once.
-    core_query := '
-SELECT  mbe.id,
-        mbe.value,
-        mbe.sort_value
-  FROM  metabib.browse_entry mbe
-  WHERE (
-            EXISTS ( -- are there any bibs using this mbe via the requested fields?
-                SELECT  1
-                  FROM  metabib.browse_entry_def_map mbedm
-                  WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
-                  LIMIT 1
-            ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
-                SELECT  1
-                  FROM  metabib.browse_entry_simple_heading_map mbeshm
-                        JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
-                        JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
-                            ash.atag = map.authority_field
-                            AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
-                        )
-                  WHERE mbeshm.entry = mbe.id
-            )
-        ) AND ';
-
-    -- This is the variant of the query for browsing backward.
-    back_query := core_query ||
-        ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
-    ' ORDER BY mbe.sort_value DESC, mbe.value DESC ';
-
-    -- This variant browses forward.
-    forward_query := core_query ||
-        ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
-    ' ORDER BY mbe.sort_value, mbe.value ';
-
-    -- We now call the function which applies a cursor to the provided
-    -- queries, stopping at the appropriate limits and also giving us
-    -- the next page's pivot.
-    RETURN QUERY
-        SELECT * FROM metabib.staged_browse(
-            back_query, search_field, context_org, context_locations,
-            staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
-        ) UNION
-        SELECT * FROM metabib.staged_browse(
-            forward_query, search_field, context_org, context_locations,
-            staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
-        ) ORDER BY row_number DESC;
-
-END;
-$p$ LANGUAGE PLPGSQL;
-
-CREATE OR REPLACE FUNCTION metabib.browse(
-    search_class        TEXT,
-    browse_term         TEXT,
-    context_org         INT DEFAULT NULL,
-    context_loc_group   INT DEFAULT NULL,
-    staff               BOOL DEFAULT FALSE,
-    pivot_id            BIGINT DEFAULT NULL,
-    result_limit        INT DEFAULT 10
-) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
-BEGIN
-    RETURN QUERY SELECT * FROM metabib.browse(
-        (SELECT COALESCE(ARRAY_AGG(id), ARRAY[]::INT[])
-            FROM config.metabib_field WHERE field_class = search_class),
-        browse_term,
-        context_org,
-        context_loc_group,
-        staff,
-        pivot_id,
-        result_limit
-    );
-END;
-$p$ LANGUAGE PLPGSQL;
-
 -- This function is used to help clean up facet labels. Due to quirks in
 -- MARC parsing, some facet labels may be generated with periods or commas
 -- at the end.  This will strip a trailing commas off all the time, and
diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index 267b302..364702e 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -536,6 +536,15 @@ CREATE TABLE asset.copy_template (
 	mint_condition BOOL
 );
 
+CREATE TABLE asset.copy_vis_attr_cache (
+    id              BIGSERIAL   PRIMARY KEY,
+    record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
+    target_copy     BIGINT      NOT NULL,
+    vis_attr_vector INT[]
+);
+CREATE INDEX copy_vis_attr_cache_record_idx ON asset.copy_vis_attr_cache (record);
+CREATE INDEX copy_vis_attr_cache_copy_idx ON asset.copy_vis_attr_cache (target_copy);
+
 CREATE OR REPLACE FUNCTION asset.opac_ou_record_copy_count (org INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
 DECLARE
     ans RECORD;
diff --git a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql
index 8aa18be..a58ed1a 100644
--- a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql
+++ b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql
@@ -437,24 +437,30 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
-CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes TEXT[], hits BIGINT[]) RETURNS TABLE (id INT, value TEXT, count BIGINT) AS $$
-    SELECT id, value, count FROM (
-        SELECT mfae.field AS id,
-               mfae.value,
-               COUNT(DISTINCT mmrsm.source),
-               row_number() OVER (
-                PARTITION BY mfae.field ORDER BY COUNT(distinct mmrsm.source) DESC
-               ) AS rownum
-        FROM metabib.facet_entry mfae
-        JOIN metabib.metarecord_source_map mmrsm ON (mfae.source = mmrsm.source)
-        JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
-        WHERE mmrsm.source IN (SELECT * FROM unnest($2))
-        AND cmf.facet_field
-        AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
-        GROUP by 1, 2
-    ) all_facets
-    WHERE rownum <= (SELECT COALESCE((SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled), 1000));
-$$ LANGUAGE SQL;
+CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes text[], hits bigint[]) RETURNS TABLE(id integer, value text, count bigint)
+AS $f$
+    SELECT id, value, count
+      FROM (
+        SELECT  mfae.field AS id,
+                mfae.value,
+                COUNT(DISTINCT mfae.source),
+                row_number() OVER (
+                    PARTITION BY mfae.field ORDER BY COUNT(DISTINCT mfae.source) DESC
+                ) AS rownum
+          FROM  metabib.facet_entry mfae
+                JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
+          WHERE mfae.source = ANY ($2)
+                AND cmf.facet_field
+                AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
+          GROUP by 1, 2
+      ) all_facets
+      WHERE rownum <= (
+        SELECT COALESCE(
+            (SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled),
+            1000
+        )
+      );
+$f$ LANGUAGE SQL;
 
 CREATE OR REPLACE FUNCTION search.facets_for_metarecord_set(ignore_facet_classes TEXT[], hits BIGINT[]) RETURNS TABLE (id INT, value TEXT, count BIGINT) AS $$
     SELECT id, value, count FROM (
@@ -475,4 +481,777 @@ CREATE OR REPLACE FUNCTION search.facets_for_metarecord_set(ignore_facet_classes
     WHERE rownum <= (SELECT COALESCE((SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled), 1000));
 $$ LANGUAGE SQL;
 
+CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute ( value INT, attr TEXT ) RETURNS INT AS $f$
+SELECT  ((CASE $2
+
+            WHEN 'luri_org'         THEN 0 -- "b" attr
+            WHEN 'bib_source'       THEN 1 -- "b" attr
+
+            WHEN 'copy_flags'       THEN 0 -- "c" attr
+            WHEN 'owning_lib'       THEN 1 -- "c" attr
+            WHEN 'circ_lib'         THEN 2 -- "c" attr
+            WHEN 'status'           THEN 3 -- "c" attr
+            WHEN 'location'         THEN 4 -- "c" attr
+            WHEN 'location_group'   THEN 5 -- "c" attr
+
+        END) << 28 ) | $1;
+
+/* copy_flags bit positions, LSB-first:
+
+ 0: asset.copy.opac_visible
+
+
+   When adding flags, you must update asset.all_visible_flags()
+
+   Because bib and copy values are stored separately, we can reuse
+   shifts, saving us some space. We could probably take back a bit
+   too, but I'm not sure its worth squeezing that last one out. We'd
+   be left with just 2 slots for copy attrs, rather than 10.
+*/
+
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_list ( attr TEXT, value INT[] ) RETURNS INT[] AS $f$
+    SELECT ARRAY_AGG(search.calculate_visibility_attribute(x, $1)) FROM UNNEST($2) AS X;
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_test ( attr TEXT, value INT[], negate BOOL DEFAULT FALSE ) RETURNS TEXT AS $f$
+    SELECT  CASE WHEN $3 THEN '!' ELSE '' END || '(' || ARRAY_TO_STRING(search.calculate_visibility_attribute_list($1,$2),'|') || ')';
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION asset.calculate_copy_visibility_attribute_set ( copy_id BIGINT ) RETURNS INT[] AS $f$
+DECLARE
+    copy_row    asset.copy%ROWTYPE;
+    lgroup_map  asset.copy_location_group_map%ROWTYPE;
+    attr_set    INT[];
+BEGIN
+    SELECT * INTO copy_row FROM asset.copy WHERE id = copy_id;
+
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.opac_visible::INT, 'copy_flags');
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.circ_lib, 'circ_lib');
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.status, 'status');
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.location, 'location');
+
+    SELECT  ARRAY_APPEND(
+                attr_set,
+                search.calculate_visibility_attribute(owning_lib, 'owning_lib')
+            ) INTO attr_set
+      FROM  asset.call_number
+      WHERE id = copy_row.call_number;
+
+    FOR lgroup_map IN SELECT * FROM asset.copy_location_group_map WHERE location = copy_row.location LOOP
+        attr_set := attr_set || search.calculate_visibility_attribute(lgroup_map.lgroup, 'location_group');
+    END LOOP;
+
+    RETURN attr_set;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.calculate_bib_visibility_attribute_set ( bib_id BIGINT ) RETURNS INT[] AS $f$
+DECLARE
+    bib_row     biblio.record_entry%ROWTYPE;
+    cn_row      asset.call_number%ROWTYPE;
+    attr_set    INT[];
+BEGIN
+    SELECT * INTO bib_row FROM biblio.record_entry WHERE id = bib_id;
+
+    IF bib_row.source IS NOT NULL THEN
+        attr_set := attr_set || search.calculate_visibility_attribute(bib_row.source, 'bib_source');
+    END IF;
+
+    FOR cn_row IN
+        SELECT  cn.*
+          FROM  asset.call_number cn
+                JOIN asset.uri_call_number_map m ON (cn.id = m.call_number)
+                JOIN asset.uri u ON (u.id = m.uri)
+          WHERE cn.record = bib_id
+                AND cn.label = '##URI##'
+                AND u.active
+    LOOP
+        attr_set := attr_set || search.calculate_visibility_attribute(cn_row.owning_lib, 'luri_org');
+    END LOOP;
+
+    RETURN attr_set;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
+DECLARE
+    ocn     asset.call_number%ROWTYPE;
+    ncn     asset.call_number%ROWTYPE;
+    cid     BIGINT;
+BEGIN
+
+    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN -- Only needs ON INSERT OR DELETE, so handle separately
+        IF TG_OP = 'INSERT' THEN
+            INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
+                NEW.peer_record,
+                NEW.target_copy,
+                asset.calculate_copy_visibility_attribute_set(NEW.target_copy)
+            );
+
+            RETURN NEW;
+        ELSIF TG_OP = 'DELETE' THEN
+            DELETE FROM asset.copy_vis_attr_cache
+              WHERE record = NEW.peer_record AND target_copy = NEW.target_copy;
+
+            RETURN OLD;
+        END IF;
+    END IF;
+
+    IF TG_OP = 'INSERT' THEN -- Handles ON INSERT. ON UPDATE is below.
+        IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+            SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
+            INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
+                ncn.record,
+                NEW.id,
+                asset.calculate_copy_visibility_attribute_set(NEW.id)
+            );
+        ELSIF TG_TABLE_NAME = 'record_entry' THEN
+            NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
+        END IF;
+
+        RETURN NEW;
+    END IF;
+
+    -- handle items first, since with circulation activity
+    -- their statuses change frequently
+    IF TG_TABLE_NAME IN ('copy', 'unit') THEN -- This handles ON UPDATE OR DELETE. ON INSERT above
+
+        IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
+            DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
+            RETURN OLD;
+        END IF;
+
+        SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
+
+        IF OLD.deleted <> NEW.deleted THEN
+            IF NEW.deleted THEN
+                DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
+            ELSE
+                INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
+                    ncn.record,
+                    NEW.id,
+                    asset.calculate_copy_visibility_attribute_set(NEW.id)
+                );
+            END IF;
+
+            RETURN NEW;
+        ELSIF OLD.call_number  <> NEW.call_number THEN
+            SELECT * INTO ocn FROM asset.call_number cn WHERE id = OLD.call_number;
+
+            IF ncn.record <> ocn.record THEN
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(ncn.record)
+                  WHERE id = ocn.record;
+            END IF;
+        END IF;
+
+        IF OLD.location     <> NEW.location OR
+           OLD.status       <> NEW.status OR
+           OLD.opac_visible <> NEW.opac_visible OR
+           OLD.circ_lib     <> NEW.circ_lib
+        THEN
+            -- any of these could change visibility, but
+            -- we'll save some queries and not try to calculate
+            -- the change directly
+            UPDATE  asset.copy_vis_attr_cache
+              SET   target_copy = NEW.id,
+                    vis_attr_vector = asset.calculate_copy_visibility_attribute_set(NEW.id)
+              WHERE target_copy = OLD.id;
+
+        END IF;
+
+    ELSIF TG_TABLE_NAME = 'call_number' THEN -- Only ON UPDATE. Copy handler will deal with ON INSERT OR DELETE.
+
+        IF OLD.record <> NEW.record THEN
+            IF NEW.label = '##URI##' THEN
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
+                  WHERE id = OLD.record;
+
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(NEW.record)
+                  WHERE id = NEW.record;
+            END IF;
+
+            UPDATE  asset.copy_vis_attr_cache
+              SET   record = NEW.record,
+                    vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
+              WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
+                    AND record = OLD.record;
+
+        ELSIF OLD.owning_lib <> NEW.owning_lib THEN
+            UPDATE  asset.copy_vis_attr_cache
+              SET   vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
+              WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
+                    AND record = NEW.record;
+
+            IF NEW.label = '##URI##' THEN
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
+                  WHERE id = OLD.record;
+            END IF;
+        END IF;
+
+    ELSIF TG_TABLE_NAME = 'record_entry' THEN -- Only handles ON UPDATE OR DELETE
+
+        IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
+            DELETE FROM asset.copy_vis_attr_cache WHERE record = OLD.id;
+            RETURN OLD;
+        ELSIF OLD.source <> NEW.source THEN
+            NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
+        END IF;
+
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER z_opac_vis_mat_view_tgr BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+
+CREATE OR REPLACE FUNCTION asset.all_visible_flags () RETURNS TEXT AS $f$
+    SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(1 << x, 'copy_flags')),'&') || ')'
+      FROM  GENERATE_SERIES(0,0) AS x; -- increment as new flags are added.
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.visible_orgs (otype TEXT) RETURNS TEXT AS $f$
+    SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
+      FROM  actor.org_unit
+      WHERE opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.invisible_orgs (otype TEXT) RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
+      FROM  actor.org_unit
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+-- Bib-oriented defaults for search
+CREATE OR REPLACE FUNCTION asset.bib_source_default () RETURNS TEXT AS $f$
+    SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'bib_source')),'|') || ')'
+      FROM  config.bib_source
+      WHERE transcendant;
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION asset.luri_org_default () RETURNS TEXT AS $f$
+    SELECT  * FROM asset.invisible_orgs('luri_org');
+$f$ LANGUAGE SQL STABLE;
+
+-- Copy-oriented defaults for search
+CREATE OR REPLACE FUNCTION asset.location_group_default () RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location_group')),'|') || ')'
+      FROM  asset.copy_location_group
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.location_default () RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location')),'|') || ')'
+      FROM  asset.copy_location
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.status_default () RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'status')),'|') || ')'
+      FROM  config.copy_status
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.owning_lib_default () RETURNS TEXT AS $f$
+    SELECT  * FROM asset.invisible_orgs('owning_lib');
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.circ_lib_default () RETURNS TEXT AS $f$
+    SELECT  * FROM asset.invisible_orgs('circ_lib');
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.patron_default_visibility_mask () RETURNS TABLE (b_attrs TEXT, c_attrs TEXT)  AS $f$
+DECLARE
+    copy_flags      TEXT; -- "c" attr
+
+    owning_lib      TEXT; -- "c" attr
+    circ_lib        TEXT; -- "c" attr
+    status          TEXT; -- "c" attr
+    location        TEXT; -- "c" attr
+    location_group  TEXT; -- "c" attr
+
+    luri_org        TEXT; -- "b" attr
+    bib_sources     TEXT; -- "b" attr
+BEGIN
+    copy_flags      := asset.all_visible_flags(); -- Will always have at least one
+
+    owning_lib      := NULLIF(asset.owning_lib_default(),'!()');
+
+    circ_lib        := NULLIF(asset.circ_lib_default(),'!()');
+    status          := NULLIF(asset.status_default(),'!()');
+    location        := NULLIF(asset.location_default(),'!()');
+    location_group  := NULLIF(asset.location_group_default(),'!()');
+
+    luri_org        := NULLIF(asset.luri_org_default(),'!()');
+    bib_sources     := NULLIF(asset.bib_source_default(),'()');
+
+    RETURN QUERY SELECT
+        '('||ARRAY_TO_STRING(
+            ARRAY[luri_org,bib_sources],
+            '|'
+        )||')',
+        '('||ARRAY_TO_STRING(
+            ARRAY[copy_flags,owning_lib,circ_lib,status,location,location_group]::TEXT[],
+            '&'
+        )||')';
+END;
+$f$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION metabib.suggest_browse_entries(raw_query_text text, search_class text, headline_opts text, visibility_org integer, query_limit integer, normalization integer)
+ RETURNS TABLE(value text, field integer, buoyant_and_class_match boolean, field_match boolean, field_weight integer, rank real, buoyant boolean, match text)
+AS $f$
+DECLARE
+    prepared_query_texts    TEXT[];
+    query                   TSQUERY;
+    plain_query             TSQUERY;
+    opac_visibility_join    TEXT;
+    search_class_join       TEXT;
+    r_fields                RECORD;
+BEGIN
+    prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
+
+    query := TO_TSQUERY('keyword', prepared_query_texts[1]);
+    plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
+
+    visibility_org := NULLIF(visibility_org,-1);
+    IF visibility_org IS NOT NULL THEN
+        PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
+        IF FOUND THEN
+            opac_visibility_join := '';
+        ELSE
+            opac_visibility_join := '
+    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
+    JOIN vm ON (acvac.vis_attr_vector @@
+            (vm.c_attrs || $$&$$ ||
+                search.calculate_visibility_attribute_test(
+                    $$circ_lib$$,
+                    (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
+                )
+            )::query_int
+         )
+';
+        END IF;
+    ELSE
+        opac_visibility_join := '';
+    END IF;
+
+    -- The following determines whether we only provide suggestsons matching
+    -- the user's selected search_class, or whether we show other suggestions
+    -- too. The reason for MIN() is that for search_classes like
+    -- 'title|proper|uniform' you would otherwise get multiple rows.  The
+    -- implication is that if title as a class doesn't have restrict,
+    -- nor does the proper field, but the uniform field does, you're going
+    -- to get 'false' for your overall evaluation of 'should we restrict?'
+    -- To invert that, change from MIN() to MAX().
+
+    SELECT
+        INTO r_fields
+            MIN(cmc.restrict::INT) AS restrict_class,
+            MIN(cmf.restrict::INT) AS restrict_field
+        FROM metabib.search_class_to_registered_components(search_class)
+            AS _registered (field_class TEXT, field INT)
+        JOIN
+            config.metabib_class cmc ON (cmc.name = _registered.field_class)
+        LEFT JOIN
+            config.metabib_field cmf ON (cmf.id = _registered.field);
+
+    -- evaluate 'should we restrict?'
+    IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
+        search_class_join := '
+    JOIN
+        metabib.search_class_to_registered_components($2)
+        AS _registered (field_class TEXT, field INT) ON (
+            (_registered.field IS NULL AND
+                _registered.field_class = cmf.field_class) OR
+            (_registered.field = cmf.id)
+        )
+    ';
+    ELSE
+        search_class_join := '
+    LEFT JOIN
+        metabib.search_class_to_registered_components($2)
+        AS _registered (field_class TEXT, field INT) ON (
+            _registered.field_class = cmc.name
+        )
+    ';
+    END IF;
+
+    RETURN QUERY EXECUTE '
+WITH vm AS ( SELECT * FROM asset.patron_default_visibility_mask() ),
+     mbe AS (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000)
+SELECT  DISTINCT
+        x.value,
+        x.id,
+        x.push,
+        x.restrict,
+        x.weight,
+        x.ts_rank_cd,
+        x.buoyant,
+        TS_HEADLINE(value, $7, $3)
+  FROM  (SELECT DISTINCT
+                mbe.value,
+                cmf.id,
+                cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
+                _registered.field = cmf.id AS restrict,
+                cmf.weight,
+                TS_RANK_CD(mbe.index_vector, $1, $6),
+                cmc.buoyant,
+                mbedm.source
+          FROM  metabib.browse_entry_def_map mbedm
+                JOIN mbe ON (mbe.id = mbedm.entry)
+                JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
+                JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
+                '  || search_class_join || '
+          ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
+          LIMIT 1000) AS x
+        ' || opac_visibility_join || '
+  ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
+  LIMIT $5
+'   -- sic, repeat the order by clause in the outer select too
+    USING
+        query, search_class, headline_opts,
+        visibility_org, query_limit, normalization, plain_query
+        ;
+
+    -- sort order:
+    --  buoyant AND chosen class = match class
+    --  chosen field = match field
+    --  field weight
+    --  rank
+    --  buoyancy
+    --  value itself
+
+END;
+$f$ LANGUAGE plpgsql ROWS 10;
+
+CREATE OR REPLACE FUNCTION metabib.staged_browse(query text, fields integer[], context_org integer, context_locations integer[], staff boolean, browse_superpage_size integer, count_up_from_zero boolean, result_limit integer, next_pivot_pos integer)
+ RETURNS SETOF metabib.flat_browse_entry_appearance
+AS $f$
+DECLARE
+    curs                    REFCURSOR;
+    rec                     RECORD;
+    qpfts_query             TEXT;
+    aqpfts_query            TEXT;
+    afields                 INT[];
+    bfields                 INT[];
+    result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
+    results_skipped         INT := 0;
+    row_counter             INT := 0;
+    row_number              INT;
+    slice_start             INT;
+    slice_end               INT;
+    full_end                INT;
+    all_records             BIGINT[];
+    all_brecords             BIGINT[];
+    all_arecords            BIGINT[];
+    superpage_of_records    BIGINT[];
+    superpage_size          INT;
+    c_tests                 TEXT := '';
+    b_tests                 TEXT := '';
+    c_orgs                  INT[];
+BEGIN
+    IF count_up_from_zero THEN
+        row_number := 0;
+    ELSE
+        row_number := -1;
+    END IF;
+
+    IF NOT staff THEN
+        SELECT x.c_attrs, x.b_attrs INTO c_tests, b_tests FROM asset.patron_default_visibility_mask() x;
+    END IF;
+
+    IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
+    IF b_tests <> '' THEN b_tests := b_tests || '&'; END IF;
+
+    SELECT ARRAY_AGG(id) INTO c_orgs FROM actor.org_unit_descendants(context_org);
+
+    c_tests := c_tests || search.calculate_visibility_attribute_test('circ_lib',c_orgs)
+               || '&' || search.calculate_visibility_attribute_test('owning_lib',c_orgs);
+
+    PERFORM 1 FROM config.internal_flag WHERE enabled AND name = 'opac.located_uri.act_as_copy';
+    IF FOUND THEN
+        b_tests := b_tests || search.calculate_visibility_attribute_test(
+            'luri_org',
+            (SELECT ARRAY_AGG(id) FROM actor.org_unit_full_path(context_org) x)
+        );
+    ELSE
+        b_tests := b_tests || search.calculate_visibility_attribute_test(
+            'luri_org',
+            (SELECT ARRAY_AGG(id) FROM actor.org_unit_ancestors(context_org) x)
+        );
+    END IF;
+
+    IF context_locations THEN
+        IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
+        c_tests := c_tests || search.calculate_visibility_attribute_test('location',context_locations);
+    END IF;
+
+    OPEN curs NO SCROLL FOR EXECUTE query;
+
+    LOOP
+        FETCH curs INTO rec;
+        IF NOT FOUND THEN
+            IF result_row.pivot_point IS NOT NULL THEN
+                RETURN NEXT result_row;
+            END IF;
+            RETURN;
+        END IF;
+
+        -- Gather aggregate data based on the MBE row we're looking at now, authority axis
+        SELECT INTO all_arecords, result_row.sees, afields
+                ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
+                STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
+                ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
+
+          FROM  metabib.browse_entry_simple_heading_map mbeshm
+                JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
+                JOIN authority.authority_linking aal ON ( ash.record = aal.source )
+                JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
+                JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
+                    ash.atag = map.authority_field
+                    AND map.metabib_field = ANY(fields)
+                )
+          WHERE mbeshm.entry = rec.id;
+
+        -- Gather aggregate data based on the MBE row we're looking at now, bib axis
+        SELECT INTO all_brecords, result_row.authorities, bfields
+                ARRAY_AGG(DISTINCT source),
+                STRING_AGG(DISTINCT authority::TEXT, $$,$$),
+                ARRAY_AGG(DISTINCT def)
+          FROM  metabib.browse_entry_def_map
+          WHERE entry = rec.id
+                AND def = ANY(fields);
+
+        SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
+
+        result_row.sources := 0;
+        result_row.asources := 0;
+
+        -- Bib-linked vis checking
+        IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
+
+            SELECT  INTO result_row.sources COUNT(DISTINCT b.id)
+              FROM  biblio.record_entry b
+                    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
+              WHERE b.id = ANY(all_brecords[1:browse_superpage_size])
+                    AND (
+                        acvac.vis_attr_vector @@ c_tests::query_int
+                        OR b.vis_attr_vector @@ b_tests::query_int
+                    );
+
+            result_row.accurate := TRUE;
+
+        END IF;
+
+        -- Authority-linked vis checking
+        IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
+
+            SELECT  INTO result_row.asources COUNT(DISTINCT b.id)
+              FROM  biblio.record_entry b
+                    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
+              WHERE b.id = ANY(all_arecords[1:browse_superpage_size])
+                    AND (
+                        acvac.vis_attr_vector @@ c_tests::query_int
+                        OR b.vis_attr_vector @@ b_tests::query_int
+                    );
+
+            result_row.aaccurate := TRUE;
+
+        END IF;
+
+        IF result_row.sources > 0 OR result_row.asources > 0 THEN
+
+            -- The function that calls this function needs row_number in order
+            -- to correctly order results from two different runs of this
+            -- functions.
+            result_row.row_number := row_number;
+
+            -- Now, if row_counter is still less than limit, return a row.  If
+            -- not, but it is less than next_pivot_pos, continue on without
+            -- returning actual result rows until we find
+            -- that next pivot, and return it.
+
+            IF row_counter < result_limit THEN
+                result_row.browse_entry := rec.id;
+                result_row.value := rec.value;
+
+                RETURN NEXT result_row;
+            ELSE
+                result_row.browse_entry := NULL;
+                result_row.authorities := NULL;
+                result_row.fields := NULL;
+                result_row.value := NULL;
+                result_row.sources := NULL;
+                result_row.sees := NULL;
+                result_row.accurate := NULL;
+                result_row.aaccurate := NULL;
+                result_row.pivot_point := rec.id;
+
+                IF row_counter >= next_pivot_pos THEN
+                    RETURN NEXT result_row;
+                    RETURN;
+                END IF;
+            END IF;
+
+            IF count_up_from_zero THEN
+                row_number := row_number + 1;
+            ELSE
+                row_number := row_number - 1;
+            END IF;
+
+            -- row_counter is different from row_number.
+            -- It simply counts up from zero so that we know when
+            -- we've reached our limit.
+            row_counter := row_counter + 1;
+        END IF;
+    END LOOP;
+END;
+$f$ LANGUAGE plpgsql ROWS 10;
+
+CREATE OR REPLACE FUNCTION metabib.browse(search_field integer[], browse_term text, context_org integer DEFAULT NULL::integer, context_loc_group integer DEFAULT NULL::integer, staff boolean DEFAULT false, pivot_id bigint DEFAULT NULL::bigint, result_limit integer DEFAULT 10)
+ RETURNS SETOF metabib.flat_browse_entry_appearance
+AS $f$
+DECLARE
+    core_query              TEXT;
+    back_query              TEXT;
+    forward_query           TEXT;
+    pivot_sort_value        TEXT;
+    pivot_sort_fallback     TEXT;
+    context_locations       INT[];
+    browse_superpage_size   INT;
+    results_skipped         INT := 0;
+    back_limit              INT;
+    back_to_pivot           INT;
+    forward_limit           INT;
+    forward_to_pivot        INT;
+BEGIN
+    -- First, find the pivot if we were given a browse term but not a pivot.
+    IF pivot_id IS NULL THEN
+        pivot_id := metabib.browse_pivot(search_field, browse_term);
+    END IF;
+
+    SELECT INTO pivot_sort_value, pivot_sort_fallback
+        sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
+
+    -- Bail if we couldn't find a pivot.
+    IF pivot_sort_value IS NULL THEN
+        RETURN;
+    END IF;
+
+    -- Transform the context_loc_group argument (if any) (logc at the
+    -- TPAC layer) into a form we'll be able to use.
+    IF context_loc_group IS NOT NULL THEN
+        SELECT INTO context_locations ARRAY_AGG(location)
+            FROM asset.copy_location_group_map
+            WHERE lgroup = context_loc_group;
+    END IF;
+
+    -- Get the configured size of browse superpages.
+    SELECT INTO browse_superpage_size COALESCE(value::INT,100)     -- NULL ok
+        FROM config.global_flag
+        WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
+
+    -- First we're going to search backward from the pivot, then we're going
+    -- to search forward.  In each direction, we need two limits.  At the
+    -- lesser of the two limits, we delineate the edge of the result set
+    -- we're going to return.  At the greater of the two limits, we find the
+    -- pivot value that would represent an offset from the current pivot
+    -- at a distance of one "page" in either direction, where a "page" is a
+    -- result set of the size specified in the "result_limit" argument.
+    --
+    -- The two limits in each direction make four derived values in total,
+    -- and we calculate them now.
+    back_limit := CEIL(result_limit::FLOAT / 2);
+    back_to_pivot := result_limit;
+    forward_limit := result_limit / 2;
+    forward_to_pivot := result_limit - 1;
+
+    -- This is the meat of the SQL query that finds browse entries.  We'll
+    -- pass this to a function which uses it with a cursor, so that individual
+    -- rows may be fetched in a loop until some condition is satisfied, without
+    -- waiting for a result set of fixed size to be collected all at once.
+    core_query := '
+SELECT  mbe.id,
+        mbe.value,
+        mbe.sort_value
+  FROM  metabib.browse_entry mbe
+  WHERE (
+            EXISTS ( -- are there any bibs using this mbe via the requested fields?
+                SELECT  1
+                  FROM  metabib.browse_entry_def_map mbedm
+                  WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
+            ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
+                SELECT  1
+                  FROM  metabib.browse_entry_simple_heading_map mbeshm
+                        JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
+                        JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
+                            ash.atag = map.authority_field
+                            AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
+                        )
+                  WHERE mbeshm.entry = mbe.id
+            )
+        ) AND ';
+
+    -- This is the variant of the query for browsing backward.
+    back_query := core_query ||
+        ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
+    ' ORDER BY mbe.sort_value DESC, mbe.value DESC LIMIT 1000';
+
+    -- This variant browses forward.
+    forward_query := core_query ||
+        ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
+    ' ORDER BY mbe.sort_value, mbe.value LIMIT 1000';
+
+    -- We now call the function which applies a cursor to the provided
+    -- queries, stopping at the appropriate limits and also giving us
+    -- the next page's pivot.
+    RETURN QUERY
+        SELECT * FROM metabib.staged_browse(
+            back_query, search_field, context_org, context_locations,
+            staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
+        ) UNION
+        SELECT * FROM metabib.staged_browse(
+            forward_query, search_field, context_org, context_locations,
+            staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
+        ) ORDER BY row_number DESC;
+
+END;
+$f$ LANGUAGE plpgsql ROWS 10;
+
+CREATE OR REPLACE FUNCTION metabib.browse(
+    search_class        TEXT,
+    browse_term         TEXT,
+    context_org         INT DEFAULT NULL,
+    context_loc_group   INT DEFAULT NULL,
+    staff               BOOL DEFAULT FALSE,
+    pivot_id            BIGINT DEFAULT NULL,
+    result_limit        INT DEFAULT 10
+) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
+BEGIN
+    RETURN QUERY SELECT * FROM metabib.browse(
+        (SELECT COALESCE(ARRAY_AGG(id), ARRAY[]::INT[])
+            FROM config.metabib_field WHERE field_class = search_class),
+        browse_term,
+        context_org,
+        context_loc_group,
+        staff,
+        pivot_id,
+        result_limit
+    );
+END;
+$p$ LANGUAGE PLPGSQL ROWS 10;
+
+
 COMMIT;
diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql
index a58ce64..c922a93 100644
--- a/Open-ILS/src/sql/Pg/999.functions.global.sql
+++ b/Open-ILS/src/sql/Pg/999.functions.global.sql
@@ -1216,273 +1216,6 @@ BEGIN
 END;
 $func$ LANGUAGE plpgsql;
 
-
--- copy OPAC visibility materialized view
-CREATE OR REPLACE FUNCTION asset.refresh_opac_visible_copies_mat_view () RETURNS VOID AS $$
-
-    TRUNCATE TABLE asset.opac_visible_copies;
-
-    INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
-    SELECT  cp.id, cp.circ_lib, cn.record
-    FROM  asset.copy cp
-        JOIN asset.call_number cn ON (cn.id = cp.call_number)
-        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
-        JOIN asset.copy_location cl ON (cp.location = cl.id)
-        JOIN config.copy_status cs ON (cp.status = cs.id)
-        JOIN biblio.record_entry b ON (cn.record = b.id)
-    WHERE NOT cp.deleted
-        AND NOT cl.deleted
-        AND NOT cn.deleted
-        AND NOT b.deleted
-        AND cs.opac_visible
-        AND cl.opac_visible
-        AND cp.opac_visible
-        AND a.opac_visible
-            UNION
-    SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record
-    FROM  asset.copy cp
-        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
-        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
-        JOIN asset.copy_location cl ON (cp.location = cl.id)
-        JOIN config.copy_status cs ON (cp.status = cs.id)
-    WHERE NOT cp.deleted
-        AND NOT cl.deleted
-        AND cs.opac_visible
-        AND cl.opac_visible
-        AND cp.opac_visible
-        AND a.opac_visible;
-
-$$ LANGUAGE SQL;
-COMMENT ON FUNCTION asset.refresh_opac_visible_copies_mat_view() IS $$
-Rebuild the copy OPAC visibility cache.  Useful during migrations.
-$$;
-
-CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
-DECLARE
-    add_front       TEXT;
-    add_back        TEXT;
-    add_base_query  TEXT;
-    add_peer_query  TEXT;
-    remove_query    TEXT;
-    do_add          BOOLEAN := false;
-    do_remove       BOOLEAN := false;
-BEGIN
-    add_base_query := $$
-        SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
-          FROM  asset.copy cp
-                JOIN asset.call_number cn ON (cn.id = cp.call_number)
-                JOIN actor.org_unit a ON (cp.circ_lib = a.id)
-                JOIN asset.copy_location cl ON (cp.location = cl.id)
-                JOIN config.copy_status cs ON (cp.status = cs.id)
-                JOIN biblio.record_entry b ON (cn.record = b.id)
-          WHERE NOT cp.deleted
-                AND NOT cl.deleted
-                AND NOT cn.deleted
-                AND NOT b.deleted
-                AND cs.opac_visible
-                AND cl.opac_visible
-                AND cp.opac_visible
-                AND a.opac_visible
-    $$;
-    add_peer_query := $$
-        SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
-          FROM  asset.copy cp
-                JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
-                JOIN actor.org_unit a ON (cp.circ_lib = a.id)
-                JOIN asset.copy_location cl ON (cp.location = cl.id)
-                JOIN config.copy_status cs ON (cp.status = cs.id)
-          WHERE NOT cp.deleted
-                AND NOT cl.deleted
-                AND cs.opac_visible
-                AND cl.opac_visible
-                AND cp.opac_visible
-                AND a.opac_visible
-    $$;
-    add_front := $$
-        INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
-          SELECT DISTINCT ON (id, record) id, circ_lib, record FROM (
-    $$;
-    add_back := $$
-        ) AS x
-    $$;
- 
-    remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
-
-    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
-        IF TG_OP = 'INSERT' THEN
-            add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.peer_record = ' || NEW.peer_record;
-            EXECUTE add_front || add_peer_query || add_back;
-            RETURN NEW;
-        ELSE
-            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
-            EXECUTE remove_query;
-            RETURN OLD;
-        END IF;
-    END IF;
-
-    IF TG_OP = 'INSERT' THEN
-
-        IF TG_TABLE_NAME IN ('copy', 'unit') THEN
-            add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
-            EXECUTE add_front || add_base_query || add_back;
-        END IF;
-
-        RETURN NEW;
-
-    END IF;
-
-    -- handle items first, since with circulation activity
-    -- their statuses change frequently
-    IF TG_TABLE_NAME IN ('copy', 'unit') THEN
-
-        IF OLD.location    <> NEW.location OR
-           OLD.call_number <> NEW.call_number OR
-           OLD.status      <> NEW.status OR
-           OLD.circ_lib    <> NEW.circ_lib THEN
-            -- any of these could change visibility, but
-            -- we'll save some queries and not try to calculate
-            -- the change directly
-            do_remove := true;
-            do_add := true;
-        ELSE
-
-            IF OLD.deleted <> NEW.deleted THEN
-                IF NEW.deleted THEN
-                    do_remove := true;
-                ELSE
-                    do_add := true;
-                END IF;
-            END IF;
-
-            IF OLD.opac_visible <> NEW.opac_visible THEN
-                IF OLD.opac_visible THEN
-                    do_remove := true;
-                ELSIF NOT do_remove THEN -- handle edge case where deleted item
-                                        -- is also marked opac_visible
-                    do_add := true;
-                END IF;
-            END IF;
-
-        END IF;
-
-        IF do_remove THEN
-            DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
-        END IF;
-        IF do_add THEN
-            add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
-            add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
-            EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
-        END IF;
-
-        RETURN NEW;
-
-    END IF;
-
-    IF TG_TABLE_NAME IN ('call_number', 'copy_location', 'record_entry') THEN -- these have a 'deleted' column
- 
-        IF OLD.deleted AND NEW.deleted THEN -- do nothing
-
-            RETURN NEW;
- 
-        ELSIF NEW.deleted THEN -- remove rows
- 
-            IF TG_TABLE_NAME = 'call_number' THEN
-                DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
-            ELSIF TG_TABLE_NAME = 'copy_location' THEN
-                DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE location = NEW.id);
-            ELSIF TG_TABLE_NAME = 'record_entry' THEN
-                DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
-            END IF;
- 
-            RETURN NEW;
- 
-        ELSIF OLD.deleted THEN -- add rows
- 
-            IF TG_TABLE_NAME = 'call_number' THEN
-                add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
-                EXECUTE add_front || add_base_query || add_back;
-            ELSIF TG_TABLE_NAME = 'copy_location' THEN
-                add_base_query := add_base_query || 'AND cl.id = ' || NEW.id;
-                EXECUTE add_front || add_base_query || add_back;
-            ELSIF TG_TABLE_NAME = 'record_entry' THEN
-                add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
-                add_peer_query := add_peer_query || ' AND pbcm.peer_record = ' || NEW.id;
-                EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
-            END IF;
- 
-            RETURN NEW;
- 
-        END IF;
- 
-    END IF;
-
-    IF TG_TABLE_NAME = 'call_number' THEN
-
-        IF OLD.record <> NEW.record THEN
-            -- call number is linked to different bib
-            remove_query := remove_query || 'call_number = ' || NEW.id || ');';
-            EXECUTE remove_query;
-            add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
-            EXECUTE add_front || add_base_query || add_back;
-        END IF;
-
-        RETURN NEW;
-
-    END IF;
-
-    IF TG_TABLE_NAME IN ('record_entry') THEN
-        RETURN NEW; -- don't have 'opac_visible'
-    END IF;
-
-    -- actor.org_unit, asset.copy_location, asset.copy_status
-    IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
-
-        RETURN NEW;
-
-    ELSIF NEW.opac_visible THEN -- add rows
-
-        IF TG_TABLE_NAME = 'org_unit' THEN
-            add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id;
-            add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id;
-        ELSIF TG_TABLE_NAME = 'copy_location' THEN
-            add_base_query := add_base_query || ' AND cp.location = ' || NEW.id;
-            add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id;
-        ELSIF TG_TABLE_NAME = 'copy_status' THEN
-            add_base_query := add_base_query || ' AND cp.status = ' || NEW.id;
-            add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id;
-        END IF;
- 
-        EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
- 
-    ELSE -- delete rows
-
-        IF TG_TABLE_NAME = 'org_unit' THEN
-            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
-        ELSIF TG_TABLE_NAME = 'copy_location' THEN
-            remove_query := remove_query || 'location = ' || NEW.id || ');';
-        ELSIF TG_TABLE_NAME = 'copy_status' THEN
-            remove_query := remove_query || 'status = ' || NEW.id || ');';
-        END IF;
- 
-        EXECUTE remove_query;
- 
-    END IF;
- 
-    RETURN NEW;
-END;
-$func$ LANGUAGE PLPGSQL;
-COMMENT ON FUNCTION asset.cache_copy_visibility() IS $$
-Trigger function to update the copy OPAC visiblity cache.
-$$;
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy_location FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON config.copy_status FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-CREATE TRIGGER a_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON actor.org_unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
-
 -- Authority ingest routines
 CREATE OR REPLACE FUNCTION authority.propagate_changes 
     (aid BIGINT, bid BIGINT) RETURNS BIGINT AS $func$
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
index c554f24..e2e1fcd 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
@@ -219,7 +219,7 @@ BEGIN
             SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
             INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
                 ncn.record,
-                NEW.target_copy,
+                NEW.id,
                 asset.calculate_copy_visibility_attribute_set(NEW.id)
             );
         ELSIF TG_TABLE_NAME = 'record_entry' THEN

commit 7319e93ca989f2abccd15b1aff70ab5904aa3cab
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jul 19 14:07:39 2017 -0400

    LP#1698206: Make use of current search library in autosuggest
    
    Here we teach autosuggest how to check the opac search scope.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index f79d09e..fb9a83f 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -1826,11 +1826,22 @@ BEGIN
 
     visibility_org := NULLIF(visibility_org,-1);
     IF visibility_org IS NOT NULL THEN
-        opac_visibility_join := '
-    JOIN asset.opac_visible_copies aovc ON (
-        aovc.record = x.source AND
-        aovc.circ_lib IN (SELECT id FROM actor.org_unit_descendants($4))
-    )';
+        PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
+        IF FOUND THEN
+            opac_visibility_join := '';
+        ELSE
+            opac_visibility_join := '
+    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
+    JOIN vm ON (acvac.vis_attr_vector @@
+            (vm.c_attrs || $$&$$ ||
+                search.calculate_visibility_attribute_test(
+                    $$circ_lib$$,
+                    (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
+                )
+            )::query_int
+         )
+';
+        END IF;
     ELSE
         opac_visibility_join := '';
     END IF;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
index 906c82a..c554f24 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
@@ -437,10 +437,22 @@ BEGIN
 
     visibility_org := NULLIF(visibility_org,-1);
     IF visibility_org IS NOT NULL THEN
-        opac_visibility_join := '
+        PERFORM FROM actor.org_unit WHERE id = visibility_org AND parent_ou IS NULL;
+        IF FOUND THEN
+            opac_visibility_join := '';
+        ELSE
+            opac_visibility_join := '
     JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
-    JOIN vm ON (acvac.vis_attr_vector @@ vm.c_attrs::query_int)
+    JOIN vm ON (acvac.vis_attr_vector @@
+            (vm.c_attrs || $$&$$ ||
+                search.calculate_visibility_attribute_test(
+                    $$circ_lib$$,
+                    (SELECT ARRAY_AGG(id) FROM actor.org_unit_descendants($4))
+                )
+            )::query_int
+         )
 ';
+        END IF;
     ELSE
         opac_visibility_join := '';
     END IF;

commit abdf6f8ebc385996be871594af8985a2fc07b4a2
Author: Mike Rylander <miker at esilibrary.com>
Date:   Mon Jul 10 10:35:07 2017 -0400

    LP#1698206: Indicate broad searches and heavy facets
    
    There is a configurable, pagable limit on hits, defined by the superpage
    size and max superpages.  When the hit count equals this (by default,
    100000) we add a '+' to the hit count to show that there are even more hits.
    
    Facets are calculated per superpage, and if the facet use count equals the
    number of superpages seen so far multiplied by the superpage size, we
    likewise add '+' to indicate that there are likely more records matching the
    facet.
    
    For facets, a user can page far enough to increase the visible number, if
    they cross superpage boundaries.
    
    Signed-off-by: Mike Rylander <miker at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index facc53d..4a0336d 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -1293,6 +1293,7 @@ sub staged_search {
             global_summary    => $global_summary,
             count             => $global_summary->{visible},
             core_limit        => $search_hash->{core_limit},
+            superpage         => $page,
             superpage_size    => $search_hash->{check_limit},
             superpage_summary => $current_page_summary,
             facet_key         => $facet_key,
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
index fa72bc9..6cc9437 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -470,6 +470,9 @@ sub load_rresults {
 
     $ctx->{ids} = $rec_ids;
     $ctx->{hit_count} = $results->{count};
+    $ctx->{superpage} = $results->{superpage};
+    $ctx->{superpage_size} = $results->{superpage_size};
+    $ctx->{pagable_limit} = $results->{core_limit};
     $ctx->{query_struct} = $results->{global_summary}{query_struct};
     $logger->debug('query struct: '. Dumper($ctx->{query_struct}));
     $ctx->{canonicalized_query} = $results->{global_summary}{canonicalized_query};
diff --git a/Open-ILS/src/templates/opac/parts/result/facets.tt2 b/Open-ILS/src/templates/opac/parts/result/facets.tt2
index c8c7196..38f231f 100644
--- a/Open-ILS/src/templates/opac/parts/result/facets.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/facets.tt2
@@ -120,7 +120,7 @@ FOR facet IN sorted_facets;
                               href="[% mkurl('', {facet => new_facets}) %]" rel="nofollow" vocab="">[% display_value %]</a>
                             [% END %]
                         </div>
-                        <div class="count">([% facet_data.count %])</div>
+                        <div class="count">([% facet_data.count; IF facet_data.count == (ctx.superpage + 1) * ctx.superpage_size; '+'; END %])</div>
                     </div>
                 [% 
                     ELSE;
diff --git a/Open-ILS/src/templates/opac/parts/result/paginate.tt2 b/Open-ILS/src/templates/opac/parts/result/paginate.tt2
index 00e1fb2..ba66454 100644
--- a/Open-ILS/src/templates/opac/parts/result/paginate.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/paginate.tt2
@@ -2,6 +2,7 @@
 <div class="results_header_nav1">
     <span class="h1">[% ctx.bookbag ? l('List Contents') : l('Search Results') %]</span>
     <span class="result_number">
+                [%~ IF ctx.hit_count == ctx.pagable_limit; ctx.hit_count = ctx.hit_count _ '+'; END ~%]
                 [%~ |l('<span class="result_count_number">' _ ctx.result_start _'</span>',
                 '<span class="result_count_number">' _ ctx.result_stop _ '</span>',
                 '<span class="result_count_number">' _ ctx.hit_count _ '</span>')  ~%]

commit ec22696f691b30a085e6f6e4d0ed95de7e63d42e
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jun 28 18:13:30 2017 -0400

    LP#1698206: Add TechRef documentation from commit message
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/TechRef/PureSQLSearch.adoc b/docs/TechRef/PureSQLSearch.adoc
new file mode 100644
index 0000000..3f4306b
--- /dev/null
+++ b/docs/TechRef/PureSQLSearch.adoc
@@ -0,0 +1,197 @@
+== Pure-SQL searching
+
+=== Background
+Evergreen stores all data, including that useful for patron and staff search,
+in a normalized schema that is time and space efficient for transactional use
+cases, and provides guarantees on data integrity.  In addition, development is
+made simpler than would be the case otherwise and arbitrary reporting is made
+possible.
+
+However, this structure is not effective for direct, SQL-only search
+functionality in a hierarchical, consortial dataset.  This is a problem that
+is relatively unique to Evergreen, as it is most often employed to host and
+serve large consortia with overlapping bibliographic datasets and
+non-overlapping item and location datasets.  Other search engines, including
+those built into other ILSs, do not generally have to account for
+hierarchically organized location visibility concerns as a primary use case.
+In other words, because it provides functionality that requires a hierarchical
+view of non-bibliographic data, a problem space for Evergreen is essentially
+nonexistent in competing products.
+
+Evergreen's search infrastructure has evolved over the years.  In its current
+form, the software first performs a full text search against extracted
+bibliographic data and limits this initial internal result set to a
+configurable size.  It then investigates the visibility of each result on
+several non-bibliographic axes.  These visibility tests take up the
+preponderance of CPU time spent in search, with full text search of the
+bibliographic data generally completing within milliseconds. The main reason
+this multi-stage mechanism is used is that there are many visibility axes and
+attempting to join all required data sources together in a single query will
+cause the search use case to perform very poorly.  A previous attempt to
+create a pure SQL search mechanism failed for this reason.
+
+A significant drawback of the current approach is that the costs imposed by
+visibility filtering search results using normalized non-bibliographic data,
+either in-query or separated from the main full-text query as it is today,
+make it necessary to place limits on the number of database rows matched by
+full-text query constructs.  This in turn can cause searches to omit results
+in certain situations, such as a large consortium consisting of a few large
+libraries and many small libraries.
+
+However, it has been shown possible to overcome this performance issue by
+providing an extensible way to collect all visibility related information
+together into a small number of novel data structures with a compact in-memory
+representation and very fast comparison functions.  In this way, we are able
+to use pure SQL search strategies and therefore avoid result visibility
+problems while also benefiting from improvements to the core PostgreSQL
+database engine.  Further, this will open the door to indexing improvements,
+such as removal of the need for duplicate data storage, or the use of non-XML
+data storage schemes, which could reduce resource requirements and have a
+direct, positive effect on patron and staff search experience.
+
+=== Overview of existing search logic
+
+. Construct core bibliographic search query
+. Collect non-bibliographic filtering criteria
+. Pass query and filters to a database function
+. Calculate hierarchical location information for visibility testing
+. Open cursor over core query, limited to *superpage_size * max_superpages* records
+. Core query implements bib-level sorting
+. For each result
+.. NEXT if not on requested superpage
+.. Check deleted flag, based on search type
+.. Check transcendence
+... Return result if true
+.. Check for direct Located URI in scope
+... Return result if exists
+.. Check copy status + (circ lib | owning lib) based on modifier
+.. Check peer bib copy status + (circ lib | owning lib) based on modifier
+.. Check copy location based on filter
+.. Check peer bib copy location based on filter
+.. General copy visibility checks
+... If NOT staff
+.... Check for OPAC visible copies (trigger-maintained materialization)
+.... Check for peer bib OPAC visible copies
+... If staff
+.... Confirm no copies here
+.... Confirm no peer bib map
+.... Confirm no copies anywhere
+.... Confirm no Located URIs elsewhere
+.. Return result if not excluded
+. Calculate summary row
+
+=== Overview of new mechanism
+Record and copy information (everything checked in *(7)* above) is collected
+into a novel data structure that allows all visibility-indicating criteria to
+be flattened to integer arrays.  This is facilitated by a database trigger in
+much the same way that basic OPAC copy visibility is collected for copies
+today.
+
+Most identifiers in Evergreen are stored as signed integers of either 32 or 64
+bits.  The smaller 32 bit space allows for approximately two billion positive
+entries, but all identifiers for table rows that are used as visibility axes
+fall into a range of between one and one million for all applicable use cases,
+and all identifiers of interest are positive.  Therefore, we can make use of
+the most significant bits in an integer value to create a per-axis namespacing
+mask.  When applied to the idenfitifer for a visibility axis identifier, this
+mask allows two values that are identical across axis to be identified as
+unique within a combined set of all values.
+
+Sepcifically, we retain the four most significant bits of the integer space
+and create from that 16 potential bitmasks for per-axis segregation of
+identifiers.  Further, we separate copy-centered axes and bibliographic
+record-centered attributes into two separate columns for storage purposes,
+which means we can use the same four bits for different purposes within each
+copy or bib set.
+
+In order to implement existing visibility tests with this infrastructure, six
+copy axes and two record axes are used from the possible 16 from each set.
+See the search.calculate_visibility_attribute() for details.  By using 32 bit
+integers we can collect all of the bitmasked values of each type (copy or bib)
+into a single integer array and leverage the Postgres intarray extension to
+test all axes at once.
+
+At search time, required and user-requested visibility restrictions are
+converted to *query_int* values. Results are directly filtered based on these
+calculated *query_int* values.  This works in a way analogous to record
+attribute filtering, avoiding the need to test statuses, circ and owning
+library visibility, copy locations and location groups, copy OPAC visibility,
+peer bibliographic record, Located URIs, or bibliographic record sources
+directly.
+
+=== Minimum Postgres version requirement
+Due to features, particularly functions, available only in 9.4 and newer that
+are key to the performance of the new method, Postgres 9.4 will need to be the
+new lowest supported version for use with Evergreen.  While some of the new
+features and functions could be implemented as user-defined functions in
+PL/PGSQL, they would not be fast enough to make this pure-SQL search viable.
+
+Among the important improvements that Postgres 9.4 and newer versions bring to
+Evergreen are:
+
+* Version 9.4 improved GIN indexes in ways that directly benefit Evergreen, as well as how anti-joins are planned which matters for some Evergreen searches.
+* Version 9.5 introduced many general performance improvements, especially for joins and sorting, and brought planner improvements that impact complex queries such as those generated by this code.
+* Version 9.6 delivered more general performance improvements, particularly for large servers such as those that Evergreen databases tend to live on, as well as more improvements to GIN indexes, executor changes that can avoid unnecessary work in search queries, new built-in full-text phrase searching, and initial parallel query execution.
+
+=== Performance
+The cost of the non-bibliographic filter value caching maintenance process is
+10-40% faster than existing partial caching logic which it would replace.
+
+The new code achieves up to 10% faster search times than the old, suboptimal
+mechanism time for broad searches.  The new code is faster for more selective
+searches, often by up to 90% faster.  In both broad and narrow search cases
+the new mechanism performs with complete accuracy and does not miss
+small-collection hits in large consortia as the existing code does.
+
+Unsurprisingly, and in addition to the above improvements, performance is
+improved marginally as each successive Postgres version at and beyond 9.4.
+
+=== Page rendering changes
+Previously, Evergreen would request the record details for a user-visible page
+of results in parallel, and then, serially, request the facet data for the
+result set.  Now, the facet data is requested asyncronously in the background
+and then a single feed containing all records on a result page is requested
+syncronously.  By parallelizing the result and facet metadata, page rendering
+time is cut down significantly.  Concurrent requests of the same bibliographic
+record are shared between apache backends to reduce result request time, and by
+making one request instead of ten simultaineously, database load is reduced.  A
+performance improvement of up to 20% in post-search page rendering time is seen
+from this change.
+
+Additionally, cross-apache caching of ancillary data, such as the coded value
+map and other data, via memcache significantly reduces the average page
+rendering time not just for result pages, but most pages generated by
+Evergreen.  An additional performance improvement of up to 50% in post-search
+page rendering time is seen from this change.
+
+While these changes are not directly related to the removal staged search, they
+touch areas impacted by core search changes and provided enough improvement
+that implementing them concurrently with the elimination of staged search
+seemed optimal.
+
+=== User visible configuration changes
+The stock configuration now provides an increased value for *max_superpages*
+in opensrf.xml.  The default is now 100, and the *superpage_size* remains
+1000, for a total limit of 100,000 hits per search.  This is not a limit on
+visibility per se, as all records are visibility tested and ranked before
+limiting, but simply a limit on the number of pages a user could click through
+before reaching the end of the presented result list.
+
+=== Tuning sensitivity
+User-level timeouts are still possible with both the old and new code, given a
+large enough dataset, a broad enough query, and a cold cache.  However, the
+*gin_fuzzy_search_limit* GUC can be used to set a time cap on the new
+mechanism. See https://www.postgresql.org/docs/9.6/static/gin-tips.html for
+background, though the suggested values in the documentation are significantly
+lower than would be readily useful for a large Evergreen instance.
+
+Because it uses a more complex query structure, the new mechanism is somewhat
+more sensitive to Postgres tuning in general.  In particular, lowering
+*random_page_cost* from the default of *4.0* to a more reasonable *2.0* is
+important for proper query planning.  For Evergreen use cases where the search
+indexes and relevant tables are kept in RAM or SSDs are used for storage, this
+value is acceptable and useful in general.
+
+=== Funding and development
+This project was funded by MassLNC and developed by Equinox Open Library
+Initiative.

commit 688c2a26d0921ba06dd633835348bdda3989591b
Author: Mike Rylander <miker at esilibrary.com>
Date:   Wed Jun 28 18:07:21 2017 -0400

    LP#1698206: Remove hit estimation cruft
    
    The old code needed to refine the estimated hit count as each superpage was
    read, but we don't need any of that anymore.  Also, it was causing hit count
    display issues on superpages after the first.  So, we do away with all that.
    
    Signed-off-by: Mike Rylander <miker at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index 361b359..facc53d 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -1204,10 +1204,8 @@ sub staged_search {
     # fulfill the user-specified limit and offset
     my $all_results = [];
     my $page; # current superpage
-    my $est_hit_count = 0;
     my $current_page_summary = {};
     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
-    my $is_real_hit_count = 0;
     my $new_ids = [];
 
     for($page = 0; $page < $max_superpages; $page++) {
@@ -1229,6 +1227,7 @@ sub staged_search {
             $logger->debug("staged search: fetching results from the database");
             $search_hash->{skip_check} = $page * $superpage_size;
             $search_hash->{return_query} = $page == 0 ? 1 : 0;
+
             my $start = time;
             $results = $U->storagereq($method, %$search_hash);
             $search_duration = time - $start;
@@ -1242,12 +1241,6 @@ sub staged_search {
 
             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
 
-            my $hc = $summary->{estimated_hit_count} || $summary->{visible};
-            if($hc == 0) {
-                $logger->info("search returned 0 results: duration=$search_duration: params=".
-                    OpenSRF::Utils::JSON->perl2JSON($search_hash));
-            }
-
             # Create backwards-compatible result structures
             if($IAmMetabib) {
                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
@@ -1270,42 +1263,20 @@ sub staged_search {
 
         my $current_count = scalar(@$all_results);
 
-        if ($page == 0) {
-            foreach (qw/ query_struct canonicalized_query /) {
+        if ($page == 0) { # all summaries are the same, just get the first
+            for (keys %$summary) {
                 $global_summary->{$_} = $summary->{$_};
             }
         }
 
-        $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
-            if $page == 0;
-
-        $logger->debug("staged search: located $current_count, with estimated hits=".
-            ($summary->{estimated_hit_count} || "none") .
-            " : visible=" . ($summary->{visible} || "none") . ", checked=" .
-            ($summary->{checked} || "none")
-        );
-
-        if (defined($summary->{estimated_hit_count})) {
-            foreach (qw/ checked visible excluded deleted /) {
-                $global_summary->{$_} += $summary->{$_};
-            }
-            $global_summary->{total} = $summary->{total};
-        }
-
         # we've found all the possible hits
-        last if $current_count == $summary->{visible}
-            and not defined $summary->{estimated_hit_count};
+        last if $current_count == $summary->{visible};
 
         # we've found enough results to satisfy the requested limit/offset
         last if $current_count >= ($user_limit + $user_offset);
 
         # we've scanned all possible hits
-        if($summary->{checked} < $superpage_size) {
-            $est_hit_count = scalar(@$all_results);
-            # we have all possible results in hand, so we know the final hit count
-            $is_real_hit_count = 1;
-            last;
-        }
+        last if($summary->{checked} < $superpage_size);
     }
 
     # Let other backends grab our data now that we're done.
@@ -1317,27 +1288,10 @@ sub staged_search {
 
     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
 
-    # refine the estimate if we have more than one superpage
-    if ($page > 0 and not $is_real_hit_count) {
-        if ($global_summary->{checked} >= $global_summary->{total}) {
-            $est_hit_count = $global_summary->{visible};
-        } else {
-            my $updated_hit_count = $U->storagereq(
-                'open-ils.storage.fts_paging_estimate',
-                $global_summary->{checked},
-                $global_summary->{visible},
-                $global_summary->{excluded},
-                $global_summary->{deleted},
-                $global_summary->{total}
-            );
-            $est_hit_count = $updated_hit_count->{$estimation_strategy};
-        }
-    }
-
     $conn->respond_complete(
         {
             global_summary    => $global_summary,
-            count             => $est_hit_count,
+            count             => $global_summary->{visible},
             core_limit        => $search_hash->{core_limit},
             superpage_size    => $search_hash->{check_limit},
             superpage_summary => $current_page_summary,
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
index 2e724de..aacd6b2 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
@@ -3120,14 +3120,6 @@ sub query_parser_fts {
     my $deleted  = $$summary_row{deleted};
     my $excluded = $$summary_row{excluded};
 
-    my $estimate = $visible;
-    if ( $total > $checked && $checked ) {
-
-        $$summary_row{hit_estimate} = FTS_paging_estimate($self, $client, $checked, $visible, $excluded, $deleted, $total);
-        $estimate = $$summary_row{estimated_hit_count} = $$summary_row{hit_estimate}{$estimation_strategy};
-
-    }
-
     delete $$summary_row{id};
     delete $$summary_row{rel};
     delete $$summary_row{record};
@@ -3147,7 +3139,7 @@ sub query_parser_fts {
 
     $client->respond( $summary_row );
 
-    $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
+    $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $visible.",DEBUG);
 
     for my $rec (@$recs) {
         delete $$rec{checked};

commit d9fa69dee18b43d5a2efc460411c45f4064ae7cc
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Jun 15 15:54:40 2017 -0400

    LP#1698206: Eliminate Staged Search
    
    === Background
    Evergreen stores all data, including that useful for patron and staff search,
    in a normalized schema that is time and space efficient for transactional use
    cases, and provides guarantees on data integrity.  In addition, development is
    made simpler than would be the case otherwise and arbitrary reporting is made
    possible.
    
    However, this structure is not effective for direct, SQL-only search
    functionality in a hierarchical, consortial dataset.  This is a problem that
    is relatively unique to Evergreen, as it is most often employed to host and
    serve large consortia with overlapping bibliographic datasets and
    non-overlapping item and location datasets.  Other search engines, including
    those built into other ILSs, do not generally have to account for
    hierarchically organized location visibility concerns as a primary use case.
    In other words, because it provides functionality that requires a hierarchical
    view of non-bibliographic data, a problem space for Evergreen is essentially
    nonexistent in competing products.
    
    Evergreen's search infrastructure has evolved over the years.  In its current
    form, the software first performs a full text search against extracted
    bibliographic data and limits this initial internal result set to a
    configurable size.  It then investigates the visibility of each result on
    several non-bibliographic axes.  These visibility tests take up the
    preponderance of CPU time spent in search, with full text search of the
    bibliographic data generally completing within milliseconds. The main reason
    this multi-stage mechanism is used is that there are many visibility axes and
    attempting to join all required data sources together in a single query will
    cause the search use case to perform very poorly.  A previous attempt to
    create a pure SQL search mechanism failed for this reason.
    
    A significant drawback of the current approach is that the costs imposed by
    visibility filtering search results using normalized non-bibliographic data,
    either in-query or separated from the main full-text query as it is today,
    make it necessary to place limits on the number of database rows matched by
    full-text query constructs.  This in turn can cause searches to omit results
    in certain situations, such as a large consortium consisting of a few large
    libraries and many small libraries.
    
    However, it has been shown possible to overcome this performance issue by
    providing an extensible way to collect all visibility related information
    together into a small number of novel data structures with a compact in-memory
    representation and very fast comparison functions.  In this way, we are able
    to use pure SQL search strategies and therefore avoid result visibility
    problems while also benefiting from improvements to the core PostgreSQL
    database engine.  Further, this will open the door to indexing improvements,
    such as removal of the need for duplicate data storage, or the use of non-XML
    data storage schemes, which could reduce resource requirements and have a
    direct, positive effect on patron and staff search experience.
    
    === Overview of existing search logic
    
    . Construct core bibliographic search query
    . Collect non-bibliographic filtering criteria
    . Pass query and filters to a database function
    . Calculate hierarchical location information for visibility testing
    . Open cursor over core query, limited to *superpage_size * max_superpages* records
    . Core query implements bib-level sorting
    . For each result
    .. NEXT if not on requested superpage
    .. Check deleted flag, based on search type
    .. Check transcendence
    ... Return result if true
    .. Check for direct Located URI in scope
    ... Return result if exists
    .. Check copy status + (circ lib | owning lib) based on modifier
    .. Check peer bib copy status + (circ lib | owning lib) based on modifier
    .. Check copy location based on filter
    .. Check peer bib copy location based on filter
    .. General copy visibility checks
    ... If NOT staff
    .... Check for OPAC visible copies (trigger-maintained materialization)
    .... Check for peer bib OPAC visible copies
    ... If staff
    .... Confirm no copies here
    .... Confirm no peer bib map
    .... Confirm no copies anywhere
    .... Confirm no Located URIs elsewhere
    .. Return result if not excluded
    . Calculate summary row
    
    === Overview of new mechanism
    Record and copy information (everything checked in *(7)* above) is collected
    into a novel data structure that allows all visibility-indicating criteria to
    be flattened to integer arrays.  This is facilitated by a database trigger in
    much the same way that basic OPAC copy visibility is collected for copies
    today.
    
    Most identifiers in Evergreen are stored as signed integers of either 32 or 64
    bits.  The smaller 32 bit space allows for approximately two billion positive
    entries, but all identifiers for table rows that are used as visibility axes
    fall into a range of between one and one million for all applicable use cases,
    and all identifiers of interest are positive.  Therefore, we can make use of
    the most significant bits in an integer value to create a per-axis namespacing
    mask.  When applied to the idenfitifer for a visibility axis identifier, this
    mask allows two values that are identical across axis to be identified as
    unique within a combined set of all values.
    
    Sepcifically, we retain the four most significant bits of the integer space
    and create from that 16 potential bitmasks for per-axis segregation of
    identifiers.  Further, we separate copy-centered axes and bibliographic
    record-centered attributes into two separate columns for storage purposes,
    which means we can use the same four bits for different purposes within each
    copy or bib set.
    
    In order to implement existing visibility tests with this infrastructure, six
    copy axes and two record axes are used from the possible 16 from each set.
    See the search.calculate_visibility_attribute() for details.  By using 32 bit
    integers we can collect all of the bitmasked values of each type (copy or bib)
    into a single integer array and leverage the Postgres intarray extension to
    test all axes at once.
    
    At search time, required and user-requested visibility restrictions are
    converted to *query_int* values. Results are directly filtered based on these
    calculated *query_int* values.  This works in a way analogous to record
    attribute filtering, avoiding the need to test statuses, circ and owning
    library visibility, copy locations and location groups, copy OPAC visibility,
    peer bibliographic record, Located URIs, or bibliographic record sources
    directly.
    
    === Minimum Postgres version requirement
    Due to features, particularly functions, available only in 9.4 and newer that
    are key to the performance of the new method, Postgres 9.4 will need to be the
    new lowest supported version for use with Evergreen.  While some of the new
    features and functions could be implemented as user-defined functions in
    PL/PGSQL, they would not be fast enough to make this pure-SQL search viable.
    
    Among the important improvements that Postgres 9.4 and newer versions bring to
    Evergreen are:
    
    * Version 9.4 improved GIN indexes in ways that directly benefit Evergreen, as well as how anti-joins are planned which matters for some Evergreen searches.
    * Version 9.5 introduced many general performance improvements, especially for joins and sorting, and brought planner improvements that impact complex queries such as those generated by this code.
    * Version 9.6 delivered more general performance improvements, particularly for large servers such as those that Evergreen databases tend to live on, as well as more improvements to GIN indexes, executor changes that can avoid unnecessary work in search queries, new built-in full-text phrase searching, and initial parallel query execution.
    
    === Performance
    The cost of the non-bibliographic filter value caching maintenance process is
    10-40% faster than existing partial caching logic which it would replace.
    
    The new code achieves up to 10% faster search times than the old, suboptimal
    mechanism time for broad searches.  The new code is faster for more selective
    searches, often by up to 90% faster.  In both broad and narrow search cases
    the new mechanism performs with complete accuracy and does not miss
    small-collection hits in large consortia as the existing code does.
    
    Unsurprisingly, and in addition to the above improvements, performance is
    improved marginally as each successive Postgres version at and beyond 9.4.
    
    === Page rendering changes
    Previously, Evergreen would request the record details for a user-visible page
    of results in parallel, and then, serially, request the facet data for the
    result set.  Now, the facet data is requested asyncronously in the background
    and then a single feed containing all records on a result page is requested
    syncronously.  By parallelizing the result and facet metadata, page rendering
    time is cut down significantly.  Concurrent requests of the same bibliographic
    record are shared between apache backends to reduce result request time, and by
    making one request instead of ten simultaineously, database load is reduced.  A
    performance improvement of up to 20% in post-search page rendering time is seen
    from this change.
    
    Additionally, cross-apache caching of ancillary data, such as the coded value
    map and other data, via memcache significantly reduces the average page
    rendering time not just for result pages, but most pages generated by
    Evergreen.  An additional performance improvement of up to 50% in post-search
    page rendering time is seen from this change.
    
    While these changes are not directly related to the removal staged search, they
    touch areas impacted by core search changes and provided enough improvement
    that implementing them concurrently with the elimination of staged search
    seemed optimal.
    
    === User visible configuration changes
    The stock configuration now provides an increased value for *max_superpages*
    in opensrf.xml.  The default is now 100, and the *superpage_size* remains
    1000, for a total limit of 100,000 hits per search.  This is not a limit on
    visibility per se, as all records are visibility tested and ranked before
    limiting, but simply a limit on the number of pages a user could click through
    before reaching the end of the presented result list.
    
    === Tuning sensitivity
    User-level timeouts are still possible with both the old and new code, given a
    large enough dataset, a broad enough query, and a cold cache.  However, the
    *gin_fuzzy_search_limit* GUC can be used to set a time cap on the new
    mechanism. See https://www.postgresql.org/docs/9.6/static/gin-tips.html for
    background, though the suggested values in the documentation are significantly
    lower than would be readily useful for a large Evergreen instance.
    
    Because it uses a more complex query structure, the new mechanism is somewhat
    more sensitive to Postgres tuning in general.  In particular, lowering
    *random_page_cost* from the default of *4.0* to a more reasonable *2.0* is
    important for proper query planning.  For Evergreen use cases where the search
    indexes and relevant tables are kept in RAM or SSDs are used for storage, this
    value is acceptable and useful in general.
    
    === Funding and development
    This project was funded by MassLNC and developed by Equinox Open Library
    Initiative.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    
    Conflicts:
    	Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example
index bbf0c52..48a3714 100644
--- a/Open-ILS/examples/opensrf.xml.example
+++ b/Open-ILS/examples/opensrf.xml.example
@@ -683,7 +683,7 @@ vim:et:ts=4:sw=4:
                     <superpage_size>1000</superpage_size>
 
                     <!-- How many superpages to consider for searching overall. -->
-                    <max_superpages>10</max_superpages>
+                    <max_superpages>100</max_superpages>
 
                     <!-- zip code database file -->
                     <!--<zips_file>LOCALSTATEDIR/data/zips.txt</zips_file>-->
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
index a56443d..a8661cc 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
@@ -763,6 +763,18 @@ sub find_org {
     return undef;
 }
 
+sub find_org_by_shortname {
+    my( $self, $org_tree, $shortname )  = @_;
+    return undef unless $org_tree and defined $shortname;
+    return $org_tree if ( $org_tree->shortname eq $shortname );
+    return undef unless ref($org_tree->children);
+    for my $c (@{$org_tree->children}) {
+        my $o = $self->find_org_by_shortname($c, $shortname);
+        return $o if $o;
+    }
+    return undef;
+}
+
 sub fetch_non_cat_type_by_name_and_org {
     my( $self, $name, $orgId ) = @_;
     $logger->debug("Fetching non cat type $name at org $orgId");
@@ -1462,6 +1474,12 @@ sub get_org_tree {
     return $tree;
 }
 
+sub get_global_flag {
+    my($self, $flag) = @_;
+    return undef unless ($flag);
+    return OpenILS::Utils::CStoreEditor->new->retrieve_config_global_flag($flag);
+}
+
 sub get_org_descendants {
     my($self, $org_id, $depth) = @_;
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index 670076b..361b359 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -1409,7 +1409,7 @@ sub retrieve_cached_facets {
 
     eval {
         local $SIG{ALRM} = sub {die};
-        alarm(2); # we'll sleep for as much as 2s
+        alarm(4); # we'll sleep for as much as 4s
         do {
             die if $cache->get_cache($key . '_COMPLETE');
         } while (sleep(0.05));
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index babaa46..f643d5f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -841,10 +841,10 @@ sub toSQL {
         $bre_join = 'INNER JOIN biblio.record_entry bre ON m.source = bre.id AND bre.deleted';
         # The above suffices for filters too when the #deleted modifier
         # is in use.
-    } elsif ($$flat_plan{uses_bre}) {
+    } elsif ($$flat_plan{uses_bre} or !$self->find_modifier('staff')) {
         $bre_join = 'INNER JOIN biblio.record_entry bre ON m.source = bre.id';
     }
-    
+
     my $desc = 'ASC';
     $desc = 'DESC' if ($self->find_modifier('descending'));
 
@@ -963,13 +963,110 @@ sub toSQL {
     my $key = 'm.source';
     $key = 'm.metarecord' if (grep {$_->name eq 'metarecord' or $_->name eq 'metabib'} @{$self->modifiers});
 
-    my $core_limit = $self->QueryParser->core_limit || 25000;
-    $core_limit = 1 if($self->find_modifier('lucky'));
+    my $core_limit = $self->QueryParser->core_limit || 'NULL';
+    if ($self->find_modifier('lucky')) {
+        $filters{check_limit} = 1;
+        $filters{skip_check} = 0;
+    	$core_limit = 1;
+    }
+
 
     my $flat_where = $$flat_plan{where};
     if ($flat_where ne '') {
         $flat_where = "AND (\n" . ${spc} x 5 . $flat_where . "\n" . ${spc} x 4 . ")";
     }
+
+    my $final_c_attr_test;
+    my $c_attr_join = '';
+    my $c_vis_test = '';
+    my $pc_vis_test = '';
+
+    # copy visibility testing
+    if (!$self->find_modifier('staff')) {
+        $pc_vis_test = "c_attrs";
+        $c_attr_join = ",c_attr"
+    }
+
+    if ($self->find_modifier('available')) {
+        push @{$$flat_plan{vis_filter}{'c_attr'}},
+            "search.calculate_visibility_attribute_test('status','{0,7,12}')";
+    }
+
+    if (@{$$flat_plan{vis_filter}{c_attr}}) {
+        $c_vis_test = join(",",@{$$flat_plan{vis_filter}{c_attr}});
+        $c_attr_join = ',c_attr';
+    }
+
+    if ($c_vis_test or $pc_vis_test) {
+        my $vis_test = '';
+
+        if ($c_vis_test and $pc_vis_test) {
+            $vis_test = $pc_vis_test . ",". $c_vis_test;
+        } elsif ($pc_vis_test) {
+            $vis_test = $pc_vis_test;
+        } else {
+            $vis_test = $c_vis_test;
+        }
+
+        # WITH-clause just generates vis test
+        $$flat_plan{with} .= "\n," if $$flat_plan{with};
+        $$flat_plan{with} .= "c_attr AS (SELECT (ARRAY_TO_STRING(ARRAY[$vis_test],'&'))::query_int AS vis_test FROM asset.patron_default_visibility_mask() x)";
+
+        $final_c_attr_test = 'EXISTS (SELECT 1 FROM asset.copy_vis_attr_cache WHERE record = m.source AND vis_attr_vector @@ c_attr.vis_test)';
+        if (!$pc_vis_test) { # staff search
+            $final_c_attr_test = '(' . $final_c_attr_test . ' OR NOT EXISTS (SELECT 1 FROM asset.copy_vis_attr_cache WHERE record = m.source))';
+        }
+    }
+ 
+    my $final_b_attr_test;
+    my $b_attr_join = '';
+    my $b_vis_test = '';
+    my $pb_vis_test = '';
+
+    # bib visibility testing
+    if (!$self->find_modifier('staff')) {
+        $pb_vis_test = "b_attrs";
+        $b_attr_join = ",b_attr"
+    }
+
+    if (@{$$flat_plan{vis_filter}{b_attr}}) {
+        $b_attr_join = ',b_attr ';
+        $b_vis_test = join("||'&'||",@{$$flat_plan{vis_filter}{b_attr}});
+    }
+
+    if ($b_vis_test or $pb_vis_test) {
+        my $vis_test = '';
+
+        if ($b_vis_test and $pb_vis_test) {
+            $vis_test = $pb_vis_test . ",". $b_vis_test;
+        } elsif ($pb_vis_test) {
+            $vis_test = $pb_vis_test;
+        } else {
+            $vis_test = $b_vis_test;
+        }
+
+        # WITH-clause just generates vis test
+        $$flat_plan{with} .= "\n," if $$flat_plan{with};
+        $$flat_plan{with} .= "b_attr AS (SELECT (ARRAY_TO_STRING(ARRAY[$vis_test],'&'))::query_int AS vis_test FROM asset.patron_default_visibility_mask() x)";
+
+        # These are magic numbers... see: search.calculate_visibility_attribute() UDF
+        $final_b_attr_test = '(b_attr.vis_test IS NULL OR bre.vis_attr_vector @@ b_attr.vis_test)';
+        if (!$pb_vis_test) { # staff search
+            $final_b_attr_test .= " OR NOT ( int4range(0,268435455,'[]') @> ANY(bre.vis_attr_vector) )";
+        }
+    }
+
+    if ($final_c_attr_test or $final_b_attr_test) { # something...
+        if ($final_c_attr_test and $final_b_attr_test) { # both!
+            my $plan = "($final_c_attr_test) OR ($final_b_attr_test)";
+            $flat_where .= "\n" . ${spc} x 4 . "AND (\n" . ${spc} x 5 .  $plan .  "\n" . ${spc} x 4 . ")";
+        } elsif ($final_c_attr_test) { # just copies...
+            $flat_where .= "\n" . ${spc} x 4 . "AND (\n" . ${spc} x 5 .  $final_c_attr_test .  "\n" . ${spc} x 4 . ")";
+        } else { # just bibs...
+            $flat_where .= "\n" . ${spc} x 4 . "AND (\n" . ${spc} x 5 .  $final_b_attr_test .  "\n" . ${spc} x 4 . ")";
+        }
+    }
+
     my $with = $$flat_plan{with};
     $with= "\nWITH $with" if $with;
 
@@ -982,27 +1079,45 @@ sub toSQL {
     }
 
     my $sql = <<SQL;
+WITH w AS (
+
 $with
-SELECT  $key AS id,
-        $agg_records,
-        (${rel})::NUMERIC AS rel,
-        $rank AS rank, 
-        FIRST(pubdate_t.value) AS tie_break,
-        STRING_AGG(ARRAY_TO_STRING(pop_with.badges,','),',') AS badges,
-        AVG(COALESCE(pop_with.total_score::NUMERIC,0.0::NUMERIC))::NUMERIC(2,1) AS popularity
-  FROM  metabib.metarecord_source_map m
-        $$flat_plan{from}
-        $mra_join
-        $mrv_join
-        $bre_join
-        $pop_join
-        $pubdate_join
-        $lang_join
-  WHERE 1=1
-        $flat_where
-  GROUP BY 1
-  ORDER BY 4 $desc $nullpos, $pop_extra_sort 5 DESC $nullpos, 3 DESC
-  LIMIT $core_limit
+SELECT  id,
+        rel,
+        CASE WHEN cardinality(records) = 1 THEN records[1] ELSE NULL END AS record,
+        NULL::INT AS total,
+        NULL::INT AS checked,
+        NULL::INT AS visible,
+        NULL::INT AS deleted,
+        NULL::INT AS excluded,
+        badges,
+        popularity
+  FROM  (SELECT $key AS id,
+                $agg_records,
+                ${rel}::NUMERIC AS rel,
+                $rank AS rank, 
+                FIRST(pubdate_t.value) AS tie_break,
+                STRING_AGG(ARRAY_TO_STRING(pop_with.badges,','),',') AS badges,
+                AVG(COALESCE(pop_with.total_score::NUMERIC,0.0::NUMERIC))::NUMERIC(2,1) AS popularity
+          FROM  metabib.metarecord_source_map m
+                $$flat_plan{from}
+                $mra_join
+                $mrv_join
+                $bre_join
+                $pop_join
+                $pubdate_join
+                $lang_join
+                $c_attr_join
+                $b_attr_join
+          WHERE 1=1
+                $flat_where
+          GROUP BY 1
+          ORDER BY 4 $desc $nullpos, $pop_extra_sort 5 DESC $nullpos, 3 DESC
+          LIMIT $core_limit
+        ) AS core_query
+) (SELECT * FROM w LIMIT $filters{check_limit} OFFSET $filters{skip_check})
+        UNION ALL
+  SELECT NULL,NULL,NULL,COUNT(*),COUNT(*),COUNT(*),0,0,NULL,NULL FROM w;
 SQL
 
     warn $sql if $self->QueryParser->debug;
@@ -1018,6 +1133,7 @@ sub flatten {
     my $from = shift || '';
     my $where = shift || '';
     my $with = '';
+    my %vis_filter = ( c_attr => [], b_attr => [] );
     my $uses_bre = 0;
     my $uses_mrd = 0;
     my $uses_mrv = 0;
@@ -1197,6 +1313,11 @@ sub flatten {
 
     my $joiner = "\n" . ${spc} x ( $self->plan_level + 5 ) . ($self->joiner eq '&' ? 'AND ' : 'OR ');
 
+    my ($depth_filter) = grep { $_->name eq 'depth' } @{$self->filters};
+    if ($depth_filter and @{$depth_filter->args} == 1) {
+        $depth_filter = $depth_filter->args->[0];
+    }
+
     my @dlist = ();
     my $common = 0;
     # for each dynamic filter, build more of the WHERE clause
@@ -1357,6 +1478,58 @@ sub flatten {
                     $where .= "$key ${NOT}IN (" . join(',', map { $self->QueryParser->quote_value($_) } @{$filter->args}) . ')';
                 }
 
+            } elsif ($filter->name eq 'site') {
+                if (@{$filter->args} == 1) {
+                    if (!defined($depth_filter) or $depth_filter > 0) { # no point in filtering by "all"
+                        my $sitename = $filter->args->[0];
+
+                        my $ot = $U->get_org_tree;
+                        my $site_org = $U->find_org_by_shortname($ot, $sitename);
+
+                        if ($site_org and $site_org->id != $ot->id) { # no point in filtering by "all"
+                            my $dorgs = $U->get_org_descendants($site_org->id, $depth_filter);
+                            my $aorgs = $U->get_org_ancestors($site_org->id);
+
+                            my $negate = $filter->negate ? 'TRUE' : 'FALSE';
+                            push @{$vis_filter{'c_attr'}},
+                                "search.calculate_visibility_attribute_test('circ_lib','{".join(',', @$dorgs)."}',$negate)";
+
+                            my $lorgs = [@$aorgs];
+                            my $luri_as_copy_gf = $U->get_global_flag('opac.located_uri.act_as_copy');
+                            push @$lorgs, @$dorgs if (
+                                $luri_as_copy_gf
+                                and $U->is_true($luri_as_copy_gf->enabled)
+                                and $U->is_true($luri_as_copy_gf->value)
+                            );
+
+                            $uses_bre = 1;
+                            push @{$vis_filter{'b_attr'}},
+                                "search.calculate_visibility_attribute_test('luri_org','{".join(',', @$lorgs)."}',$negate)";
+                        }
+                    }
+                }
+
+            } elsif ($filter->name eq 'locations') {
+                if (@{$filter->args} > 0) {
+                    my $negate = $filter->negate ? 'TRUE' : 'FALSE';
+                    push @{$vis_filter{'c_attr'}},
+                        "search.calculate_visibility_attribute_test('location','{".join(',', @{$filter->args})."}',$negate)";
+                }
+
+            } elsif ($filter->name eq 'location_groups') {
+                if (@{$filter->args} > 0) {
+                    my $negate = $filter->negate ? 'TRUE' : 'FALSE';
+                    push @{$vis_filter{'c_attr'}},
+                        "search.calculate_visibility_attribute_test('location_group','{".join(',', @{$filter->args})."}',$negate)";
+                }
+
+            } elsif ($filter->name eq 'statuses') {
+                if (@{$filter->args} > 0) {
+                    my $negate = $filter->negate ? 'TRUE' : 'FALSE';
+                    push @{$vis_filter{'c_attr'}},
+                        "search.calculate_visibility_attribute_test('status','{".join(',', @{$filter->args})."}',$negate)";
+                }
+
             } elsif ($filter->name eq 'has_browse_entry') {
                 if (@{$filter->args} >= 2) {
                     my $entry = int(shift @{$filter->args});
@@ -1401,13 +1574,11 @@ sub flatten {
                     }
                 }
             } elsif ($filter->name eq 'bib_source') {
-                $uses_bre = 1;
-
                 if (@{$filter->args} > 0) {
-                    $where .= $joiner if $where ne '';
-                    $where .= "${NOT}COALESCE(bre.source IN ("
-                           . join(',', map { $self->QueryParser->quote_value($_) } @{ $filter->args })
-                           . "), false)";
+                    $uses_bre = 1;
+                    my $negate = $filter->negate ? 'TRUE' : 'FALSE';
+                    push @{$vis_filter{'c_attr'}},
+                        "search.calculate_visibility_attribute_test('source','{".join(',', @{$filter->args})."}',$negate)";
                 }
             } elsif ($filter->name eq 'from_metarecord') {
                 if (@{$filter->args} > 0) {
@@ -1442,6 +1613,7 @@ sub flatten {
         from => $from,
         where => $where,
         with => $with,
+        vis_filter => \%vis_filter,
         uses_bre => $uses_bre,
         uses_mrv => $uses_mrv,
         uses_mrd => $uses_mrd
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
index d8a65c1..2e724de 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
@@ -3001,7 +3001,7 @@ sub query_parser_fts {
 
 
     # gather the limit or default to 10
-    my $limit = $args{check_limit} || 'NULL';
+    my $limit = $args{check_limit};
     if (my ($filter) = $query->parse_tree->find_filter('limit')) {
             $limit = $filter->args->[0] if (@{$filter->args});
     }
@@ -3011,7 +3011,7 @@ sub query_parser_fts {
 
 
     # gather the offset or default to 0
-    my $offset = $args{skip_check} || $args{offset} || 0;
+    my $offset = $args{skip_check} || $args{offset};
     if (my ($filter) = $query->parse_tree->find_filter('offset')) {
             $offset = $filter->args->[0] if (@{$filter->args});
     }
@@ -3077,7 +3077,8 @@ sub query_parser_fts {
 
     my $param_search_ou = $ou;
     my $param_depth = $depth; $param_depth = 'NULL' unless (defined($depth) and length($depth) > 0 );
-    my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
+#    my $param_core_query = "\$core_query_$$\$" . $query->parse_tree->toSQL . "\$core_query_$$\$";
+    my $param_core_query = $query->parse_tree->toSQL;
     my $param_statuses = '$${' . join(',', map { s/\$//go; "\"$_\""} @statuses) . '}$$';
     my $param_locations = '$${' . join(',', map { s/\$//go; "\"$_\""} @location) . '}$$';
     my $staff = ($self->api_name =~ /staff/ or $query->parse_tree->find_modifier('staff')) ? "'t'" : "'f'";
@@ -3085,22 +3086,27 @@ sub query_parser_fts {
     my $metarecord = ($self->api_name =~ /metabib/ or $query->parse_tree->find_modifier('metabib') or $query->parse_tree->find_modifier('metarecord')) ? "'t'" : "'f'";
     my $param_pref_ou = $pref_ou || 'NULL';
 
+#    my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
+#        SELECT  * -- bib search: $args{query}
+#          FROM  search.query_parser_fts(
+#                    $param_search_ou\:\:INT,
+#                    $param_depth\:\:INT,
+#                    $param_core_query\:\:TEXT,
+#                    $param_statuses\:\:INT[],
+#                    $param_locations\:\:INT[],
+#                    $param_offset\:\:INT,
+#                    $param_check\:\:INT,
+#                    $param_limit\:\:INT,
+#                    $metarecord\:\:BOOL,
+#                    $staff\:\:BOOL,
+#                    $deleted_search\:\:BOOL,
+#                    $param_pref_ou\:\:INT
+#                );
+#    SQL
+
     my $sth = metabib::metarecord_source_map->db_Main->prepare(<<"    SQL");
-        SELECT  * -- bib search: $args{query}
-          FROM  search.query_parser_fts(
-                    $param_search_ou\:\:INT,
-                    $param_depth\:\:INT,
-                    $param_core_query\:\:TEXT,
-                    $param_statuses\:\:INT[],
-                    $param_locations\:\:INT[],
-                    $param_offset\:\:INT,
-                    $param_check\:\:INT,
-                    $param_limit\:\:INT,
-                    $metarecord\:\:BOOL,
-                    $staff\:\:BOOL,
-                    $deleted_search\:\:BOOL,
-                    $param_pref_ou\:\:INT
-                );
+        -- bib search: $args{query}
+        $param_core_query
     SQL
 
     $sth->execute;
@@ -3268,6 +3274,27 @@ sub query_parser_fts_wrapper {
         }
     }
 
+    # gather the limit or default to 10
+    my $limit = delete($args{check_limit}) || $base_plan->superpage_size;
+    if (my ($filter) = $base_plan->parse_tree->find_filter('limit')) {
+            $limit = $filter->args->[0] if (@{$filter->args});
+    }
+    if (my ($filter) = $base_plan->parse_tree->find_filter('check_limit')) {
+            $limit = $filter->args->[0] if (@{$filter->args});
+    }
+
+    # gather the offset or default to 0
+    my $offset = delete($args{skip_check}) || delete($args{offset}) || 0;
+    if (my ($filter) = $base_plan->parse_tree->find_filter('offset')) {
+            $offset = $filter->args->[0] if (@{$filter->args});
+    }
+    if (my ($filter) = $base_plan->parse_tree->find_filter('skip_check')) {
+            $offset = $filter->args->[0] if (@{$filter->args});
+    }
+
+
+    $query = "check_limit($limit) $query" if (defined $limit);
+    $query = "skip_check($offset) $query" if (defined $offset);
     $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
     $query = "badge_orgs($borgs) $query" if ($borgs);
 
@@ -3275,9 +3302,9 @@ sub query_parser_fts_wrapper {
     $query = "site($args{org_unit}) $query" if ($args{org_unit});
     $query = "depth($args{depth}) $query" if (defined($args{depth}));
     $query = "sort($args{sort}) $query" if ($args{sort});
-    $query = "limit($args{limit}) $query" if ($args{limit});
     $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
-    $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
+#    $query = "limit($args{limit}) $query" if ($args{limit});
+#    $query = "skip_check($args{skip_check}) $query" if ($args{skip_check});
     $query = "superpage($args{superpage}) $query" if ($args{superpage});
     $query = "offset($args{offset}) $query" if ($args{offset});
     $query = "#metarecord $query" if ($self->api_name =~ /metabib/);
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
index eb268dc..9bed9cd 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -55,6 +55,7 @@ sub child_init {
 sub init_ro_object_cache {
     my $self = shift;
     my $ctx = $self->ctx;
+    my $memcache ||= OpenSRF::Utils::Cache->new('global');
 
     # reset org unit setting cache on each page load to avoid the
     # requirement of reloading apache with each org-setting change
@@ -87,12 +88,21 @@ sub init_ro_object_cache {
         my $get_key = "get_$hint";
         my $search_key = "search_$hint";
 
+        my $memcache_key = join('.', 'EGWeb',$locale,$hint) . '.';
+
         # Retrieve the full set of objects with class $hint
         $locale_subs->{$list_key} = sub {
+            my $from_memcache = 0;
+            my $list = $memcache->get_cache($memcache_key.'list');
+            if ($list) {
+                $cache{list}{$locale}{$hint} = $list;
+                $from_memcache = 1;
+            }
             my $method = "retrieve_all_$eclass";
             my $e = new_editor();
             $cache{list}{$locale}{$hint} = $e->$method() unless $cache{list}{$locale}{$hint};
             undef $e;
+            $memcache->put_cache($memcache_key.'list',$cache{list}{$locale}{$hint}) unless $from_memcache;
             return $cache{list}{$locale}{$hint};
         };
 
@@ -336,13 +346,23 @@ my $unapi_cache;
 sub get_records_and_facets {
     my ($self, $rec_ids, $facet_key, $unapi_args) = @_;
 
+    # collect the facet data
+    my $search = OpenSRF::AppSession->create('open-ils.search');
+    my $facet_req;
+    if ($facet_key) {
+        $facet_req = $search->request(
+            'open-ils.search.facet_cache.retrieve', $facet_key
+        );
+    }
+
     $unapi_args ||= {};
     $unapi_args->{site} ||= $self->ctx->{aou_tree}->()->shortname;
     $unapi_args->{depth} ||= $self->ctx->{aou_tree}->()->ou_type->depth;
     $unapi_args->{flesh_depth} ||= 5;
 
     my $is_meta = delete $unapi_args->{metarecord};
-    my $unapi_type = $is_meta ? 'unapi.mmr' : 'unapi.bre';
+    #my $unapi_type = $is_meta ? 'unapi.mmr' : 'unapi.bre';
+    my $unapi_type = $is_meta ? 'unapi.metabib_virtual_record_feed' : 'unapi.biblio_record_entry_feed';
 
     $unapi_cache ||= OpenSRF::Utils::Cache->new('global');
     my $unapi_cache_key_suffix = join(
@@ -356,134 +376,44 @@ sub get_records_and_facets {
 
     my %tmp_data;
     my $outer_self = $self;
-    $self->timelog("get_records_and_facets(): about to call multisession");
-    my $ses = OpenSRF::MultiSession->new(
-        app => 'open-ils.cstore',
-        cap => 10, # XXX config
-        success_handler => sub {
-            my($self, $req) = @_;
-            my $data = $req->{response}->[0]->content;
-
-            $outer_self->timelog("get_records_and_facets(): got response content");
-
-            # Protect against requests for non-existent records
-            return unless $data->{$unapi_type};
-
-            my $xml = XML::LibXML->new->parse_string($data->{$unapi_type})->documentElement;
-
-            $outer_self->timelog("get_records_and_facets(): parsed xml");
-            # Protect against legacy invalid MARCXML that might not have a 901c
-            my $bre_id;
-            my $mmr_id;
-            my $bre_id_nodes =  $xml->find('*[@tag="901"]/*[@code="c"]');
-            if ($bre_id_nodes) {
-                $bre_id =  $bre_id_nodes->[0]->textContent;
-            } else {
-                $logger->warn("Missing 901 subfield 'c' in " . $xml->toString());
-            }
-
-            if ($is_meta) {
-                # extract metarecord ID from mmr.unapi tag
-                for my $node ($xml->getElementsByTagName('abbr')) {
-                    my $title = $node->getAttribute('title');
-                    ($mmr_id = $title) =~ 
-                        s/tag:open-ils.org:U2\@mmr\/(\d+)\/.*/$1/g;
-                    last if $mmr_id;
-                }
-            }
 
-            my $rec_id = $mmr_id ? $mmr_id : $bre_id;
-            $tmp_data{$rec_id} = {
-                id => $rec_id, 
-                bre_id => $bre_id, 
-                mmr_id => $mmr_id,
-                marc_xml => $xml
-            };
-
-            if ($rec_id) {
-                # Let other backends grab our data now that we're done.
-                my $key = 'TPAC_unapi_cache_'.$rec_id.'_'.$unapi_cache_key_suffix;
-                my $cache_data = $unapi_cache->get_cache($key);
-                if ($$cache_data{running}) {
-                    $unapi_cache->put_cache($key, {
-                        bre_id => $bre_id,
-                        mmr_id => $mmr_id,
-                        id => $rec_id, 
-                        marc_xml => $data->{$unapi_type} 
-                    }, 10);
-                }
-            }
-
-            $outer_self->timelog("get_records_and_facets(): end of success handler");
-        }
-    );
-
-    $self->timelog("get_records_and_facets(): about to call ".
-        "$unapi_type via json_query (rec_ids has " . scalar(@$rec_ids));
-
-    my @loop_recs = uniq @$rec_ids;
-    my %rec_timeout;
+    my $sdepth = $unapi_args->{flesh_depth};
+    my $slimit = "acn=>$sdepth,acp=>$sdepth";
+    $slimit .= ",bre=>$sdepth" if $is_meta;
+    my $flesh = $unapi_args->{flesh} || '';
 
-    while (my $bid = shift @loop_recs) {
+    # tag the record with the MR id
+    $flesh =~ s/}$/,mmr.unapi}/g if $is_meta;
 
-        sleep(0.1) if $rec_timeout{$bid};
+    my $ses = OpenSRF::AppSession->create('open-ils.cstore');
 
+    my @loop_recs;
+    for my $bid (@$rec_ids) {
         my $unapi_cache_key = 'TPAC_unapi_cache_'.$bid.'_'.$unapi_cache_key_suffix;
-        my $unapi_data = $unapi_cache->get_cache($unapi_cache_key) || {};
-
-        if ($unapi_data->{running}) { #cache entry from ongoing, concurrent retrieval
-            if (!$rec_timeout{$bid}) {
-                $rec_timeout{$bid} = time() + 10;
-            }
-
-            if ( time() > $rec_timeout{$bid} ) { # we've waited too long. just do it
-                $unapi_data = {};
-                delete $rec_timeout{$bid};
-            } else { # we'll pause next time around to let this one try again
-                push(@loop_recs, $bid);
-                next;
-            }
-        }
+        my $unapi_data = $unapi_cache->get_cache($unapi_cache_key);
 
-        if ($unapi_data->{marc_xml}) { # we got data from the cache
+        if (!$unapi_data || $unapi_data->{running}) { #cache entry not done yet, get our own copy
+            push(@loop_recs, $bid);
+        } else {
             $unapi_data->{marc_xml} = XML::LibXML->new->parse_string($unapi_data->{marc_xml})->documentElement;
             $tmp_data{$unapi_data->{id}} = $unapi_data;
-        } else { # we're the first or we timed out. success_handler will populate the real value
-            $unapi_cache->put_cache($unapi_cache_key, { running => $$ }, 10);
-
-            my $sdepth = $unapi_args->{flesh_depth};
-            my $slimit = "acn=>$sdepth,acp=>$sdepth";
-            $slimit .= ",bre=>$sdepth" if $is_meta;
-            my $flesh = $unapi_args->{flesh} || '';
-
-            # tag the record with the MR id
-            $flesh =~ s/}$/,mmr.unapi}/g if $is_meta;
-
-            $ses->request(
-                'open-ils.cstore.json_query',
-                 {from => [
-                    $unapi_type, $bid, 'marcxml','record', $flesh,
-                    $unapi_args->{site}, 
-                    $unapi_args->{depth}, 
-                    $slimit,
-                    undef, undef, $unapi_args->{pref_lib}
-                ]}
-            );
         }
     }
 
-    # gather up the unapi recs
-    $ses->session_wait(1);
-    $self->timelog("get_records_and_facets():past session wait");
+    my $unapi_req = $ses->request(
+        'open-ils.cstore.json_query',
+         {from => [
+            $unapi_type, '{'.join(',', at loop_recs).'}', 'marcxml', $flesh,
+            $unapi_args->{site}, 
+            $unapi_args->{depth}, 
+            $slimit,
+            undef, undef, $unapi_args->{pref_lib}
+        ]}
+    );
 
     my $facets = {};
-    if ($facet_key) {
+    if ($facet_req) {
         $self->timelog("get_records_and_facets():almost ready to fetch facets");
-        # collect the facet data
-        my $search = OpenSRF::AppSession->create('open-ils.search');
-        my $facet_req = $search->request(
-            'open-ils.search.facet_cache.retrieve', $facet_key
-        );
 
         my $tmp_facets = $facet_req->gather(1);
         $self->timelog("get_records_and_facets(): gathered facet data");
@@ -508,10 +438,66 @@ sub get_records_and_facets {
             }
         }
         $self->timelog("get_records_and_facets(): gathered/sorted facet data");
-        $search->kill_me;
     } else {
         $facets = undef;
     }
+    $search->kill_me;
+
+    my $data = $unapi_req->gather(1);
+
+    $outer_self->timelog("get_records_and_facets(): got response content");
+
+    # Protect against requests for non-existent records
+    return unless $data->{$unapi_type};
+
+    my $doc = XML::LibXML->new->parse_string($data->{$unapi_type})->documentElement;
+
+    $outer_self->timelog("get_records_and_facets(): parsed xml");
+    for my $xml ($doc->getElementsByTagName('record')) {
+        $xml = XML::LibXML->new->parse_string($xml->toString)->documentElement;
+
+        # Protect against legacy invalid MARCXML that might not have a 901c
+        my $bre_id;
+        my $mmr_id;
+        my $bre_id_nodes =  $xml->find('*[@tag="901"]/*[@code="c"]');
+        if ($bre_id_nodes) {
+            $bre_id =  $bre_id_nodes->[0]->textContent;
+        } else {
+            $logger->warn("Missing 901 subfield 'c' in " . $xml->toString());
+        }
+    
+        if ($is_meta) {
+            # extract metarecord ID from mmr.unapi tag
+            for my $node ($xml->getElementsByTagName('abbr')) {
+                my $title = $node->getAttribute('title');
+                ($mmr_id = $title) =~ 
+                    s/tag:open-ils.org:U2\@mmr\/(\d+)\/.*/$1/g;
+                last if $mmr_id;
+            }
+        }
+    
+        my $rec_id = $mmr_id ? $mmr_id : $bre_id;
+        $tmp_data{$rec_id} = {
+            id => $rec_id, 
+            bre_id => $bre_id, 
+            mmr_id => $mmr_id,
+            marc_xml => $xml
+        };
+    
+        if ($rec_id) {
+            # Let other backends grab our data now that we're done.
+            my $key = 'TPAC_unapi_cache_'.$rec_id.'_'.$unapi_cache_key_suffix;
+            my $cache_data = $unapi_cache->get_cache($key);
+            if ($$cache_data{running}) {
+                $unapi_cache->put_cache($key, {
+                    bre_id => $bre_id,
+                    mmr_id => $mmr_id,
+                    id => $rec_id, 
+                    marc_xml => $xml->toString
+                }, 10);
+            }
+        }
+    }
 
     return ($facets, map { $tmp_data{$_} } @$rec_ids);
 }
diff --git a/Open-ILS/src/sql/Pg/990.schema.unapi.sql b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
index b1a407a..617946a 100644
--- a/Open-ILS/src/sql/Pg/990.schema.unapi.sql
+++ b/Open-ILS/src/sql/Pg/990.schema.unapi.sql
@@ -321,6 +321,8 @@ RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 
 CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$ SELECT NULL::XML $F$ LANGUAGE SQL STABLE;
+
 CREATE OR REPLACE FUNCTION unapi.memoize (classname TEXT, obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
 DECLARE
     key     TEXT;
@@ -407,6 +409,65 @@ BEGIN
 END;
 $F$ LANGUAGE PLPGSQL STABLE;
 
+CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
+DECLARE
+    layout          unapi.bre_output_layout%ROWTYPE;
+    transform       config.xml_transform%ROWTYPE;
+    item_format     TEXT;
+    tmp_xml         TEXT;
+    xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+    ouid            INT;
+    element_list    TEXT[];
+BEGIN
+
+    IF org = '-' OR org IS NULL THEN
+        SELECT shortname INTO org FROM evergreen.org_top();
+    END IF;
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+    xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+    -- Gather the bib xml
+    SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+    IF layout.title_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+    END IF;
+
+    IF layout.description_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+    END IF;
+
+    IF layout.creator_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+    END IF;
+
+    IF layout.update_ts_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+    END IF;
+
+    IF unapi_url IS NOT NULL THEN
+        EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+    END IF;
+
+    IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+    element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+    FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+        EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+    END LOOP;
+
+    RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
 CREATE OR REPLACE FUNCTION unapi.bre (
     obj_id BIGINT,
     format TEXT,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
new file mode 100644
index 0000000..906c82a
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_vis_attr_cache.sql
@@ -0,0 +1,859 @@
+BEGIN;
+
+-- Thist change drops a needless join and saves 10-15% in time cost
+CREATE OR REPLACE FUNCTION search.facets_for_record_set(ignore_facet_classes text[], hits bigint[]) RETURNS TABLE(id integer, value text, count bigint)
+AS $f$
+    SELECT id, value, count
+      FROM (
+        SELECT  mfae.field AS id,
+                mfae.value,
+                COUNT(DISTINCT mfae.source),
+                row_number() OVER (
+                    PARTITION BY mfae.field ORDER BY COUNT(DISTINCT mfae.source) DESC
+                ) AS rownum
+          FROM  metabib.facet_entry mfae
+                JOIN config.metabib_field cmf ON (cmf.id = mfae.field)
+          WHERE mfae.source = ANY ($2)
+                AND cmf.facet_field
+                AND cmf.field_class NOT IN (SELECT * FROM unnest($1))
+          GROUP by 1, 2
+      ) all_facets
+      WHERE rownum <= (
+        SELECT COALESCE(
+            (SELECT value::INT FROM config.global_flag WHERE name = 'search.max_facets_per_field' AND enabled),
+            1000
+        )
+      );
+$f$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed ( id_list BIGINT[], format TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit HSTORE DEFAULT NULL, soffset HSTORE DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE, title TEXT DEFAULT NULL, description TEXT DEFAULT NULL, creator TEXT DEFAULT NULL, update_ts TEXT DEFAULT NULL, unapi_url TEXT DEFAULT NULL, header_xml XML DEFAULT NULL ) RETURNS XML AS $F$
+DECLARE
+    layout          unapi.bre_output_layout%ROWTYPE;
+    transform       config.xml_transform%ROWTYPE;
+    item_format     TEXT;
+    tmp_xml         TEXT;
+    xmlns_uri       TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+    ouid            INT;
+    element_list    TEXT[];
+BEGIN
+
+    IF org = '-' OR org IS NULL THEN
+        SELECT shortname INTO org FROM evergreen.org_top();
+    END IF;
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+    xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+    -- Gather the bib xml
+    SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+    IF layout.title_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+    END IF;
+
+    IF layout.description_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+    END IF;
+
+    IF layout.creator_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+    END IF;
+
+    IF layout.update_ts_element IS NOT NULL THEN
+        EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+    END IF;
+
+    IF unapi_url IS NOT NULL THEN
+        EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+    END IF;
+
+    IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+    element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+    FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+        EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+    END LOOP;
+
+    RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+CREATE TABLE asset.copy_vis_attr_cache (
+    id              BIGSERIAL   PRIMARY KEY,
+    record          BIGINT      NOT NULL, -- No FKEYs, managed by user triggers.
+    target_copy     BIGINT      NOT NULL,
+    vis_attr_vector INT[]
+);
+CREATE INDEX copy_vis_attr_cache_record_idx ON asset.copy_vis_attr_cache (record);
+CREATE INDEX copy_vis_attr_cache_copy_idx ON asset.copy_vis_attr_cache (target_copy);
+
+ALTER TABLE biblio.record_entry ADD COLUMN vis_attr_vector INT[];
+
+CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute ( value INT, attr TEXT ) RETURNS INT AS $f$
+SELECT  ((CASE $2
+
+            WHEN 'luri_org'         THEN 0 -- "b" attr
+            WHEN 'bib_source'       THEN 1 -- "b" attr
+
+            WHEN 'copy_flags'       THEN 0 -- "c" attr
+            WHEN 'owning_lib'       THEN 1 -- "c" attr
+            WHEN 'circ_lib'         THEN 2 -- "c" attr
+            WHEN 'status'           THEN 3 -- "c" attr
+            WHEN 'location'         THEN 4 -- "c" attr
+            WHEN 'location_group'   THEN 5 -- "c" attr
+
+        END) << 28 ) | $1;
+
+/* copy_flags bit positions, LSB-first:
+
+ 0: asset.copy.opac_visible
+
+
+   When adding flags, you must update asset.all_visible_flags()
+
+   Because bib and copy values are stored separately, we can reuse
+   shifts, saving us some space. We could probably take back a bit
+   too, but I'm not sure its worth squeezing that last one out. We'd
+   be left with just 2 slots for copy attrs, rather than 10.
+*/
+
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_list ( attr TEXT, value INT[] ) RETURNS INT[] AS $f$
+    SELECT ARRAY_AGG(search.calculate_visibility_attribute(x, $1)) FROM UNNEST($2) AS X;
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION search.calculate_visibility_attribute_test ( attr TEXT, value INT[], negate BOOL DEFAULT FALSE ) RETURNS TEXT AS $f$
+    SELECT  CASE WHEN $3 THEN '!' ELSE '' END || '(' || ARRAY_TO_STRING(search.calculate_visibility_attribute_list($1,$2),'|') || ')';
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION asset.calculate_copy_visibility_attribute_set ( copy_id BIGINT ) RETURNS INT[] AS $f$
+DECLARE
+    copy_row    asset.copy%ROWTYPE;
+    lgroup_map  asset.copy_location_group_map%ROWTYPE;
+    attr_set    INT[];
+BEGIN
+    SELECT * INTO copy_row FROM asset.copy WHERE id = copy_id;
+
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.opac_visible::INT, 'copy_flags');
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.circ_lib, 'circ_lib');
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.status, 'status');
+    attr_set := attr_set || search.calculate_visibility_attribute(copy_row.location, 'location');
+
+    SELECT  ARRAY_APPEND(
+                attr_set,
+                search.calculate_visibility_attribute(owning_lib, 'owning_lib')
+            ) INTO attr_set
+      FROM  asset.call_number
+      WHERE id = copy_row.call_number;
+
+    FOR lgroup_map IN SELECT * FROM asset.copy_location_group_map WHERE location = copy_row.location LOOP
+        attr_set := attr_set || search.calculate_visibility_attribute(lgroup_map.lgroup, 'location_group');
+    END LOOP;
+
+    RETURN attr_set;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION biblio.calculate_bib_visibility_attribute_set ( bib_id BIGINT ) RETURNS INT[] AS $f$
+DECLARE
+    bib_row     biblio.record_entry%ROWTYPE;
+    cn_row      asset.call_number%ROWTYPE;
+    attr_set    INT[];
+BEGIN
+    SELECT * INTO bib_row FROM biblio.record_entry WHERE id = bib_id;
+
+    IF bib_row.source IS NOT NULL THEN
+        attr_set := attr_set || search.calculate_visibility_attribute(bib_row.source, 'bib_source');
+    END IF;
+
+    FOR cn_row IN
+        SELECT  cn.*
+          FROM  asset.call_number cn
+                JOIN asset.uri_call_number_map m ON (cn.id = m.call_number)
+                JOIN asset.uri u ON (u.id = m.uri)
+          WHERE cn.record = bib_id
+                AND cn.label = '##URI##'
+                AND u.active
+    LOOP
+        attr_set := attr_set || search.calculate_visibility_attribute(cn_row.owning_lib, 'luri_org');
+    END LOOP;
+
+    RETURN attr_set;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
+DECLARE
+    ocn     asset.call_number%ROWTYPE;
+    ncn     asset.call_number%ROWTYPE;
+    cid     BIGINT;
+BEGIN
+
+    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN -- Only needs ON INSERT OR DELETE, so handle separately
+        IF TG_OP = 'INSERT' THEN
+            INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
+                NEW.peer_record,
+                NEW.target_copy,
+                asset.calculate_copy_visibility_attribute_set(NEW.target_copy)
+            );
+
+            RETURN NEW;
+        ELSIF TG_OP = 'DELETE' THEN
+            DELETE FROM asset.copy_vis_attr_cache
+              WHERE record = NEW.peer_record AND target_copy = NEW.target_copy;
+
+            RETURN OLD;
+        END IF;
+    END IF;
+
+    IF TG_OP = 'INSERT' THEN -- Handles ON INSERT. ON UPDATE is below.
+        IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+            SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
+            INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
+                ncn.record,
+                NEW.target_copy,
+                asset.calculate_copy_visibility_attribute_set(NEW.id)
+            );
+        ELSIF TG_TABLE_NAME = 'record_entry' THEN
+            NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
+        END IF;
+
+        RETURN NEW;
+    END IF;
+
+    -- handle items first, since with circulation activity
+    -- their statuses change frequently
+    IF TG_TABLE_NAME IN ('copy', 'unit') THEN -- This handles ON UPDATE OR DELETE. ON INSERT above
+
+        IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
+            DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
+            RETURN OLD;
+        END IF;
+
+        SELECT * INTO ncn FROM asset.call_number cn WHERE id = NEW.call_number;
+
+        IF OLD.deleted <> NEW.deleted THEN
+            IF NEW.deleted THEN
+                DELETE FROM asset.copy_vis_attr_cache WHERE target_copy = OLD.id;
+            ELSE
+                INSERT INTO asset.copy_vis_attr_cache (record, target_copy, vis_attr_vector) VALUES (
+                    ncn.record,
+                    NEW.id,
+                    asset.calculate_copy_visibility_attribute_set(NEW.id)
+                );
+            END IF;
+
+            RETURN NEW;
+        ELSIF OLD.call_number  <> NEW.call_number THEN
+            SELECT * INTO ocn FROM asset.call_number cn WHERE id = OLD.call_number;
+
+            IF ncn.record <> ocn.record THEN
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(ncn.record)
+                  WHERE id = ocn.record;
+            END IF;
+        END IF;
+
+        IF OLD.location     <> NEW.location OR
+           OLD.status       <> NEW.status OR
+           OLD.opac_visible <> NEW.opac_visible OR
+           OLD.circ_lib     <> NEW.circ_lib
+        THEN
+            -- any of these could change visibility, but
+            -- we'll save some queries and not try to calculate
+            -- the change directly
+            UPDATE  asset.copy_vis_attr_cache
+              SET   target_copy = NEW.id,
+                    vis_attr_vector = asset.calculate_copy_visibility_attribute_set(NEW.id)
+              WHERE target_copy = OLD.id;
+
+        END IF;
+
+    ELSIF TG_TABLE_NAME = 'call_number' THEN -- Only ON UPDATE. Copy handler will deal with ON INSERT OR DELETE.
+
+        IF OLD.record <> NEW.record THEN
+            IF NEW.label = '##URI##' THEN
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
+                  WHERE id = OLD.record;
+
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(NEW.record)
+                  WHERE id = NEW.record;
+            END IF;
+
+            UPDATE  asset.copy_vis_attr_cache
+              SET   record = NEW.record,
+                    vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
+              WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
+                    AND record = OLD.record;
+
+        ELSIF OLD.owning_lib <> NEW.owning_lib THEN
+            UPDATE  asset.copy_vis_attr_cache
+              SET   vis_attr_vector = asset.calculate_copy_visibility_attribute_set(target_copy)
+              WHERE target_copy IN (SELECT id FROM asset.copy WHERE call_number = NEW.id)
+                    AND record = NEW.record;
+
+            IF NEW.label = '##URI##' THEN
+                UPDATE  biblio.record_entry
+                  SET   vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(OLD.record)
+                  WHERE id = OLD.record;
+            END IF;
+        END IF;
+
+    ELSIF TG_TABLE_NAME = 'record_entry' THEN -- Only handles ON UPDATE OR DELETE
+
+        IF TG_OP = 'DELETE' THEN -- Shouldn't get here, normally
+            DELETE FROM asset.copy_vis_attr_cache WHERE record = OLD.id;
+            RETURN OLD;
+        ELSIF OLD.source <> NEW.source THEN
+            NEW.vis_attr_vector := biblio.calculate_bib_visibility_attribute_set(NEW.id);
+        END IF;
+
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+
+-- Helper functions for use in constructing searches --
+
+CREATE OR REPLACE FUNCTION asset.all_visible_flags () RETURNS TEXT AS $f$
+    SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(1 << x, 'copy_flags')),'&') || ')'
+      FROM  GENERATE_SERIES(0,0) AS x; -- increment as new flags are added.
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.visible_orgs (otype TEXT) RETURNS TEXT AS $f$
+    SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
+      FROM  actor.org_unit
+      WHERE opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.invisible_orgs (otype TEXT) RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, $1)),'|') || ')'
+      FROM  actor.org_unit
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+-- Bib-oriented defaults for search
+CREATE OR REPLACE FUNCTION asset.bib_source_default () RETURNS TEXT AS $f$
+    SELECT  '(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'bib_source')),'|') || ')'
+      FROM  config.bib_source
+      WHERE transcendant;
+$f$ LANGUAGE SQL IMMUTABLE;
+
+CREATE OR REPLACE FUNCTION asset.luri_org_default () RETURNS TEXT AS $f$
+    SELECT  * FROM asset.invisible_orgs('luri_org');
+$f$ LANGUAGE SQL STABLE;
+
+-- Copy-oriented defaults for search
+CREATE OR REPLACE FUNCTION asset.location_group_default () RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location_group')),'|') || ')'
+      FROM  asset.copy_location_group
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.location_default () RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'location')),'|') || ')'
+      FROM  asset.copy_location
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.status_default () RETURNS TEXT AS $f$
+    SELECT  '!(' || ARRAY_TO_STRING(ARRAY_AGG(search.calculate_visibility_attribute(id, 'status')),'|') || ')'
+      FROM  config.copy_status
+      WHERE NOT opac_visible;
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.owning_lib_default () RETURNS TEXT AS $f$
+    SELECT  * FROM asset.invisible_orgs('owning_lib');
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.circ_lib_default () RETURNS TEXT AS $f$
+    SELECT  * FROM asset.invisible_orgs('circ_lib');
+$f$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION asset.patron_default_visibility_mask () RETURNS TABLE (b_attrs TEXT, c_attrs TEXT)  AS $f$
+DECLARE
+    copy_flags      TEXT; -- "c" attr
+
+    owning_lib      TEXT; -- "c" attr
+    circ_lib        TEXT; -- "c" attr
+    status          TEXT; -- "c" attr
+    location        TEXT; -- "c" attr
+    location_group  TEXT; -- "c" attr
+
+    luri_org        TEXT; -- "b" attr
+    bib_sources     TEXT; -- "b" attr
+BEGIN
+    copy_flags      := asset.all_visible_flags(); -- Will always have at least one
+
+    owning_lib      := NULLIF(asset.owning_lib_default(),'!()');
+    
+    circ_lib        := NULLIF(asset.circ_lib_default(),'!()');
+    status          := NULLIF(asset.status_default(),'!()');
+    location        := NULLIF(asset.location_default(),'!()');
+    location_group  := NULLIF(asset.location_group_default(),'!()');
+
+    luri_org        := NULLIF(asset.luri_org_default(),'!()');
+    bib_sources     := NULLIF(asset.bib_source_default(),'()');
+
+    RETURN QUERY SELECT
+        '('||ARRAY_TO_STRING(
+            ARRAY[luri_org,bib_sources],
+            '|'
+        )||')',
+        '('||ARRAY_TO_STRING(
+            ARRAY[copy_flags,owning_lib,circ_lib,status,location,location_group]::TEXT[],
+            '&'
+        )||')';
+END;
+$f$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION metabib.suggest_browse_entries(raw_query_text text, search_class text, headline_opts text, visibility_org integer, query_limit integer, normalization integer)
+ RETURNS TABLE(value text, field integer, buoyant_and_class_match boolean, field_match boolean, field_weight integer, rank real, buoyant boolean, match text)
+AS $f$
+DECLARE
+    prepared_query_texts    TEXT[];
+    query                   TSQUERY;
+    plain_query             TSQUERY;
+    opac_visibility_join    TEXT;
+    search_class_join       TEXT;
+    r_fields                RECORD;
+BEGIN
+    prepared_query_texts := metabib.autosuggest_prepare_tsquery(raw_query_text);
+
+    query := TO_TSQUERY('keyword', prepared_query_texts[1]);
+    plain_query := TO_TSQUERY('keyword', prepared_query_texts[2]);
+
+    visibility_org := NULLIF(visibility_org,-1);
+    IF visibility_org IS NOT NULL THEN
+        opac_visibility_join := '
+    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = x.source)
+    JOIN vm ON (acvac.vis_attr_vector @@ vm.c_attrs::query_int)
+';
+    ELSE
+        opac_visibility_join := '';
+    END IF;
+
+    -- The following determines whether we only provide suggestsons matching
+    -- the user's selected search_class, or whether we show other suggestions
+    -- too. The reason for MIN() is that for search_classes like
+    -- 'title|proper|uniform' you would otherwise get multiple rows.  The
+    -- implication is that if title as a class doesn't have restrict,
+    -- nor does the proper field, but the uniform field does, you're going
+    -- to get 'false' for your overall evaluation of 'should we restrict?'
+    -- To invert that, change from MIN() to MAX().
+
+    SELECT
+        INTO r_fields
+            MIN(cmc.restrict::INT) AS restrict_class,
+            MIN(cmf.restrict::INT) AS restrict_field
+        FROM metabib.search_class_to_registered_components(search_class)
+            AS _registered (field_class TEXT, field INT)
+        JOIN
+            config.metabib_class cmc ON (cmc.name = _registered.field_class)
+        LEFT JOIN
+            config.metabib_field cmf ON (cmf.id = _registered.field);
+
+    -- evaluate 'should we restrict?'
+    IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
+        search_class_join := '
+    JOIN
+        metabib.search_class_to_registered_components($2)
+        AS _registered (field_class TEXT, field INT) ON (
+            (_registered.field IS NULL AND
+                _registered.field_class = cmf.field_class) OR
+            (_registered.field = cmf.id)
+        )
+    ';
+    ELSE
+        search_class_join := '
+    LEFT JOIN
+        metabib.search_class_to_registered_components($2)
+        AS _registered (field_class TEXT, field INT) ON (
+            _registered.field_class = cmc.name
+        )
+    ';
+    END IF;
+
+    RETURN QUERY EXECUTE '
+WITH vm AS ( SELECT * FROM asset.patron_default_visibility_mask() ),
+     mbe AS (SELECT * FROM metabib.browse_entry WHERE index_vector @@ $1 LIMIT 10000)
+SELECT  DISTINCT
+        x.value,
+        x.id,
+        x.push,
+        x.restrict,
+        x.weight,
+        x.ts_rank_cd,
+        x.buoyant,
+        TS_HEADLINE(value, $7, $3)
+  FROM  (SELECT DISTINCT
+                mbe.value,
+                cmf.id,
+                cmc.buoyant AND _registered.field_class IS NOT NULL AS push,
+                _registered.field = cmf.id AS restrict,
+                cmf.weight,
+                TS_RANK_CD(mbe.index_vector, $1, $6),
+                cmc.buoyant,
+                mbedm.source
+          FROM  metabib.browse_entry_def_map mbedm
+                JOIN mbe ON (mbe.id = mbedm.entry)
+                JOIN config.metabib_field cmf ON (cmf.id = mbedm.def)
+                JOIN config.metabib_class cmc ON (cmf.field_class = cmc.name)
+                '  || search_class_join || '
+          ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
+          LIMIT 1000) AS x
+        ' || opac_visibility_join || '
+  ORDER BY 3 DESC, 4 DESC NULLS LAST, 5 DESC, 6 DESC, 7 DESC, 1 ASC
+  LIMIT $5
+'   -- sic, repeat the order by clause in the outer select too
+    USING
+        query, search_class, headline_opts,
+        visibility_org, query_limit, normalization, plain_query
+        ;
+
+    -- sort order:
+    --  buoyant AND chosen class = match class
+    --  chosen field = match field
+    --  field weight
+    --  rank
+    --  buoyancy
+    --  value itself
+
+END;
+$f$ LANGUAGE plpgsql ROWS 10;
+
+CREATE OR REPLACE FUNCTION metabib.browse(search_field integer[], browse_term text, context_org integer DEFAULT NULL::integer, context_loc_group integer DEFAULT NULL::integer, staff boolean DEFAULT false, pivot_id bigint DEFAULT NULL::bigint, result_limit integer DEFAULT 10)
+ RETURNS SETOF metabib.flat_browse_entry_appearance
+AS $f$
+DECLARE
+    core_query              TEXT;
+    back_query              TEXT;
+    forward_query           TEXT;
+    pivot_sort_value        TEXT;
+    pivot_sort_fallback     TEXT;
+    context_locations       INT[];
+    browse_superpage_size   INT;
+    results_skipped         INT := 0;
+    back_limit              INT;
+    back_to_pivot           INT;
+    forward_limit           INT;
+    forward_to_pivot        INT;
+BEGIN
+    -- First, find the pivot if we were given a browse term but not a pivot.
+    IF pivot_id IS NULL THEN
+        pivot_id := metabib.browse_pivot(search_field, browse_term);
+    END IF;
+
+    SELECT INTO pivot_sort_value, pivot_sort_fallback
+        sort_value, value FROM metabib.browse_entry WHERE id = pivot_id;
+
+    -- Bail if we couldn't find a pivot.
+    IF pivot_sort_value IS NULL THEN
+        RETURN;
+    END IF;
+
+    -- Transform the context_loc_group argument (if any) (logc at the
+    -- TPAC layer) into a form we'll be able to use.
+    IF context_loc_group IS NOT NULL THEN
+        SELECT INTO context_locations ARRAY_AGG(location)
+            FROM asset.copy_location_group_map
+            WHERE lgroup = context_loc_group;
+    END IF;
+
+    -- Get the configured size of browse superpages.
+    SELECT INTO browse_superpage_size COALESCE(value::INT,100)     -- NULL ok
+        FROM config.global_flag
+        WHERE enabled AND name = 'opac.browse.holdings_visibility_test_limit';
+
+    -- First we're going to search backward from the pivot, then we're going
+    -- to search forward.  In each direction, we need two limits.  At the
+    -- lesser of the two limits, we delineate the edge of the result set
+    -- we're going to return.  At the greater of the two limits, we find the
+    -- pivot value that would represent an offset from the current pivot
+    -- at a distance of one "page" in either direction, where a "page" is a
+    -- result set of the size specified in the "result_limit" argument.
+    --
+    -- The two limits in each direction make four derived values in total,
+    -- and we calculate them now.
+    back_limit := CEIL(result_limit::FLOAT / 2);
+    back_to_pivot := result_limit;
+    forward_limit := result_limit / 2;
+    forward_to_pivot := result_limit - 1;
+
+    -- This is the meat of the SQL query that finds browse entries.  We'll
+    -- pass this to a function which uses it with a cursor, so that individual
+    -- rows may be fetched in a loop until some condition is satisfied, without
+    -- waiting for a result set of fixed size to be collected all at once.
+    core_query := '
+SELECT  mbe.id,
+        mbe.value,
+        mbe.sort_value
+  FROM  metabib.browse_entry mbe
+  WHERE (
+            EXISTS ( -- are there any bibs using this mbe via the requested fields?
+                SELECT  1
+                  FROM  metabib.browse_entry_def_map mbedm
+                  WHERE mbedm.entry = mbe.id AND mbedm.def = ANY(' || quote_literal(search_field) || ')
+            ) OR EXISTS ( -- are there any authorities using this mbe via the requested fields?
+                SELECT  1
+                  FROM  metabib.browse_entry_simple_heading_map mbeshm
+                        JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
+                        JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
+                            ash.atag = map.authority_field
+                            AND map.metabib_field = ANY(' || quote_literal(search_field) || ')
+                        )
+                  WHERE mbeshm.entry = mbe.id
+            )
+        ) AND ';
+
+    -- This is the variant of the query for browsing backward.
+    back_query := core_query ||
+        ' mbe.sort_value <= ' || quote_literal(pivot_sort_value) ||
+    ' ORDER BY mbe.sort_value DESC, mbe.value DESC LIMIT 1000';
+
+    -- This variant browses forward.
+    forward_query := core_query ||
+        ' mbe.sort_value > ' || quote_literal(pivot_sort_value) ||
+    ' ORDER BY mbe.sort_value, mbe.value LIMIT 1000';
+
+    -- We now call the function which applies a cursor to the provided
+    -- queries, stopping at the appropriate limits and also giving us
+    -- the next page's pivot.
+    RETURN QUERY
+        SELECT * FROM metabib.staged_browse(
+            back_query, search_field, context_org, context_locations,
+            staff, browse_superpage_size, TRUE, back_limit, back_to_pivot
+        ) UNION
+        SELECT * FROM metabib.staged_browse(
+            forward_query, search_field, context_org, context_locations,
+            staff, browse_superpage_size, FALSE, forward_limit, forward_to_pivot
+        ) ORDER BY row_number DESC;
+
+END;
+$f$ LANGUAGE plpgsql ROWS 10;
+
+CREATE OR REPLACE FUNCTION metabib.staged_browse(query text, fields integer[], context_org integer, context_locations integer[], staff boolean, browse_superpage_size integer, count_up_from_zero boolean, result_limit integer, next_pivot_pos integer)
+ RETURNS SETOF metabib.flat_browse_entry_appearance
+AS $f$
+DECLARE
+    curs                    REFCURSOR;
+    rec                     RECORD;
+    qpfts_query             TEXT;
+    aqpfts_query            TEXT;
+    afields                 INT[];
+    bfields                 INT[];
+    result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
+    results_skipped         INT := 0;
+    row_counter             INT := 0;
+    row_number              INT;
+    slice_start             INT;
+    slice_end               INT;
+    full_end                INT;
+    all_records             BIGINT[];
+    all_brecords             BIGINT[];
+    all_arecords            BIGINT[];
+    superpage_of_records    BIGINT[];
+    superpage_size          INT;
+    c_tests                 TEXT := '';
+    b_tests                 TEXT := '';
+    c_orgs                  INT[];
+BEGIN
+    IF count_up_from_zero THEN
+        row_number := 0;
+    ELSE
+        row_number := -1;
+    END IF;
+
+    IF NOT staff THEN
+        SELECT x.c_attrs, x.b_attrs INTO c_tests, b_tests FROM asset.patron_default_visibility_mask() x;
+    END IF;
+
+    IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
+    IF b_tests <> '' THEN b_tests := b_tests || '&'; END IF;
+
+    SELECT ARRAY_AGG(id) INTO c_orgs FROM actor.org_unit_descendants(context_org);
+    
+    c_tests := c_tests || search.calculate_visibility_attribute_test('circ_lib',c_orgs)
+               || '&' || search.calculate_visibility_attribute_test('owning_lib',c_orgs);
+    
+    PERFORM 1 FROM config.internal_flag WHERE enabled AND name = 'opac.located_uri.act_as_copy';
+    IF FOUND THEN
+        b_tests := b_tests || search.calculate_visibility_attribute_test(
+            'luri_org',
+            (SELECT ARRAY_AGG(id) FROM actor.org_unit_full_path(context_org) x)
+        );
+    ELSE
+        b_tests := b_tests || search.calculate_visibility_attribute_test(
+            'luri_org',
+            (SELECT ARRAY_AGG(id) FROM actor.org_unit_ancestors(context_org) x)
+        );
+    END IF;
+
+    IF context_locations THEN
+        IF c_tests <> '' THEN c_tests := c_tests || '&'; END IF;
+        c_tests := c_tests || search.calculate_visibility_attribute_test('location',context_locations);
+    END IF;
+
+    OPEN curs NO SCROLL FOR EXECUTE query;
+
+    LOOP
+        FETCH curs INTO rec;
+        IF NOT FOUND THEN
+            IF result_row.pivot_point IS NOT NULL THEN
+                RETURN NEXT result_row;
+            END IF;
+            RETURN;
+        END IF;
+
+        -- Gather aggregate data based on the MBE row we're looking at now, authority axis
+        SELECT INTO all_arecords, result_row.sees, afields
+                ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
+                STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
+                ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
+
+          FROM  metabib.browse_entry_simple_heading_map mbeshm
+                JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
+                JOIN authority.authority_linking aal ON ( ash.record = aal.source )
+                JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
+                JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
+                    ash.atag = map.authority_field
+                    AND map.metabib_field = ANY(fields)
+                )
+          WHERE mbeshm.entry = rec.id;
+
+        -- Gather aggregate data based on the MBE row we're looking at now, bib axis
+        SELECT INTO all_brecords, result_row.authorities, bfields
+                ARRAY_AGG(DISTINCT source),
+                STRING_AGG(DISTINCT authority::TEXT, $$,$$),
+                ARRAY_AGG(DISTINCT def)
+          FROM  metabib.browse_entry_def_map
+          WHERE entry = rec.id
+                AND def = ANY(fields);
+
+        SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
+
+        result_row.sources := 0;
+        result_row.asources := 0;
+
+        -- Bib-linked vis checking
+        IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
+
+            SELECT  INTO result_row.sources COUNT(DISTINCT b.id)
+              FROM  biblio.record_entry b
+                    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
+              WHERE b.id = ANY(all_brecords[1:browse_superpage_size])
+                    AND (
+                        acvac.vis_attr_vector @@ c_tests::query_int
+                        OR b.vis_attr_vector @@ b_tests::query_int
+                    );
+
+            result_row.accurate := TRUE;
+
+        END IF;
+
+        -- Authority-linked vis checking
+        IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
+
+            SELECT  INTO result_row.asources COUNT(DISTINCT b.id)
+              FROM  biblio.record_entry b
+                    JOIN asset.copy_vis_attr_cache acvac ON (acvac.record = b.id)
+              WHERE b.id = ANY(all_arecords[1:browse_superpage_size])
+                    AND (
+                        acvac.vis_attr_vector @@ c_tests::query_int
+                        OR b.vis_attr_vector @@ b_tests::query_int
+                    );
+
+            result_row.aaccurate := TRUE;
+
+        END IF;
+
+        IF result_row.sources > 0 OR result_row.asources > 0 THEN
+
+            -- The function that calls this function needs row_number in order
+            -- to correctly order results from two different runs of this
+            -- functions.
+            result_row.row_number := row_number;
+
+            -- Now, if row_counter is still less than limit, return a row.  If
+            -- not, but it is less than next_pivot_pos, continue on without
+            -- returning actual result rows until we find
+            -- that next pivot, and return it.
+
+            IF row_counter < result_limit THEN
+                result_row.browse_entry := rec.id;
+                result_row.value := rec.value;
+
+                RETURN NEXT result_row;
+            ELSE
+                result_row.browse_entry := NULL;
+                result_row.authorities := NULL;
+                result_row.fields := NULL;
+                result_row.value := NULL;
+                result_row.sources := NULL;
+                result_row.sees := NULL;
+                result_row.accurate := NULL;
+                result_row.aaccurate := NULL;
+                result_row.pivot_point := rec.id;
+
+                IF row_counter >= next_pivot_pos THEN
+                    RETURN NEXT result_row;
+                    RETURN;
+                END IF;
+            END IF;
+
+            IF count_up_from_zero THEN
+                row_number := row_number + 1;
+            ELSE
+                row_number := row_number - 1;
+            END IF;
+
+            -- row_counter is different from row_number.
+            -- It simply counts up from zero so that we know when
+            -- we've reached our limit.
+            row_counter := row_counter + 1;
+        END IF;
+    END LOOP;
+END;
+$f$ LANGUAGE plpgsql ROWS 10;
+
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.peer_bib_copy_map;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON biblio.record_entry;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.call_number;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON asset.copy_location;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON serial.unit;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON config.copy_status;
+DROP TRIGGER IF EXISTS a_opac_vis_mat_view_tgr ON actor.org_unit;
+
+-- Upgrade the data!
+INSERT INTO asset.copy_vis_attr_cache (target_copy, record, vis_attr_vector)
+    SELECT  cp.id,
+            cn.record,
+            asset.calculate_copy_visibility_attribute_set(cp.id)
+      FROM  asset.copy cp
+            JOIN asset.call_number cn ON (cp.call_number = cn.id);
+
+UPDATE biblio.record_entry SET vis_attr_vector = biblio.calculate_bib_visibility_attribute_set(id);
+
+CREATE TRIGGER z_opac_vis_mat_view_tgr BEFORE INSERT OR UPDATE ON biblio.record_entry FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR DELETE ON biblio.peer_bib_copy_map FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER UPDATE ON asset.call_number FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_del_tgr BEFORE DELETE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON asset.copy FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+CREATE TRIGGER z_opac_vis_mat_view_tgr AFTER INSERT OR UPDATE ON serial.unit FOR EACH ROW EXECUTE PROCEDURE asset.cache_copy_visibility();
+
+COMMIT;
+
diff --git a/Open-ILS/src/templates/opac/parts/result/paginate.tt2 b/Open-ILS/src/templates/opac/parts/result/paginate.tt2
index 2130759..00e1fb2 100644
--- a/Open-ILS/src/templates/opac/parts/result/paginate.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/paginate.tt2
@@ -5,7 +5,7 @@
                 [%~ |l('<span class="result_count_number">' _ ctx.result_start _'</span>',
                 '<span class="result_count_number">' _ ctx.result_stop _ '</span>',
                 '<span class="result_count_number">' _ ctx.hit_count _ '</span>')  ~%]
-                Results [_1] - [_2] of about [_3]
+                Results [_1] - [_2] of [_3]
                 [%~ END %]
                 <span class='padding-left-6'>
                     [%~ |l('<span class="result_count_number">' _ (page + 1) _ '</span>',

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/opensrf.xml.example              |    2 +-
 .../perlmods/lib/OpenILS/Application/AppUtils.pm   |   18 +
 .../lib/OpenILS/Application/Search/Biblio.pm       |   61 +-
 .../Application/Storage/Driver/Pg/QueryParser.pm   |  232 ++++-
 .../Application/Storage/Publisher/action.pm        |   15 +-
 .../Application/Storage/Publisher/metabib.pm       |   80 +-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm |    3 +
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm   |  219 ++--
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/010.schema.biblio.sql          |    1 +
 Open-ILS/src/sql/Pg/030.schema.metabib.sql         |  480 +--------
 Open-ILS/src/sql/Pg/040.schema.asset.sql           |   68 +-
 Open-ILS/src/sql/Pg/300.schema.staged_search.sql   |  815 +++++++++++++-
 Open-ILS/src/sql/Pg/990.schema.unapi.sql           |   95 ++-
 Open-ILS/src/sql/Pg/999.functions.global.sql       |  267 -----
 .../Pg/upgrade/1057.schema.copy_vis_attr_cache.sql | 1214 ++++++++++++++++++++
 Open-ILS/src/support-scripts/sitemap_generator     |    8 +-
 .../src/templates/opac/parts/result/facets.tt2     |    2 +-
 .../src/templates/opac/parts/result/paginate.tt2   |    3 +-
 .../Architecture/pure_sql_searching.adoc           |    4 +
 docs/TechRef/PureSQLSearch.adoc                    |  197 ++++
 21 files changed, 2750 insertions(+), 1036 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1057.schema.copy_vis_attr_cache.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Architecture/pure_sql_searching.adoc
 create mode 100644 docs/TechRef/PureSQLSearch.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list