LP1844418 ES rebase circ 3.8 plus cross-porting
authorBill Erickson <berickxx@gmail.com>
Wed, 27 Oct 2021 16:28:16 +0000 (12:28 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 27 Oct 2021 16:28:16 +0000 (12:28 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
12 files changed:
Open-ILS/examples/elastic/README.adoc [new file with mode: 0644]
Open-ILS/examples/elastic/bib-233-marc.xml [new file with mode: 0644]
Open-ILS/examples/elastic/bib-248-marc.xml [new file with mode: 0644]
Open-ILS/examples/elastic/elastic-config.example.xml [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Elastic.pm
Open-ILS/src/perlmods/lib/OpenILS/Elastic.pm
Open-ILS/src/perlmods/lib/OpenILS/Elastic/BibSearch.pm
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.elastic-search.sql
Open-ILS/src/support-scripts/elastic-index.pl
Open-ILS/src/support-scripts/test-scripts/elastic-search-samples.pl
Open-ILS/src/support-scripts/test-scripts/elastic-search.pl
Open-ILS/xsl/elastic-bib-transform.xsl [new file with mode: 0644]

diff --git a/Open-ILS/examples/elastic/README.adoc b/Open-ILS/examples/elastic/README.adoc
new file mode 100644 (file)
index 0000000..1f46c93
--- /dev/null
@@ -0,0 +1,14 @@
+= Elasticsearch Examples
+
+== Bib Transform Testing
+
+[source,sh]
+----------------------------------------------------------------------------
+sudo apt install xsltproc
+
+xsltproc ../../xsl/elastic-bib-transform.xsl bib-248-marc.xml
+
+xsltproc ../../xsl/elastic-bib-transform.xsl bib-233-marc.xml
+----------------------------------------------------------------------------
+
+
diff --git a/Open-ILS/examples/elastic/bib-233-marc.xml b/Open-ILS/examples/elastic/bib-233-marc.xml
new file mode 100644 (file)
index 0000000..0aad4bd
--- /dev/null
@@ -0,0 +1,443 @@
+<?xml version="1.0"?>
+<record xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.loc.gov/MARC21/slim" xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd">
+  <leader>07649cim a2200913 i 4500</leader>
+  <controlfield tag="001">233</controlfield>
+  <controlfield tag="003">CONS</controlfield>
+  <controlfield tag="005">20140128084328.0</controlfield>
+  <controlfield tag="008">140128s2013    nyuopk|zqdefhi n  | ita d</controlfield>
+  <datafield tag="010" ind1=" " ind2=" ">
+    <subfield code="a">  2013565186</subfield>
+  </datafield>
+  <datafield tag="020" ind1=" " ind2=" ">
+    <subfield code="a">9781480328532</subfield>
+  </datafield>
+  <datafield tag="020" ind1=" " ind2=" ">
+    <subfield code="a">1480328537</subfield>
+  </datafield>
+  <datafield tag="024" ind1="1" ind2=" ">
+    <subfield code="a">884088883249</subfield>
+  </datafield>
+  <datafield tag="028" ind1="3" ind2="2">
+    <subfield code="a">HL50498721</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(bk.)</subfield>
+  </datafield>
+  <datafield tag="028" ind1="0" ind2="2">
+    <subfield code="a">HL50490487</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(cd.)</subfield>
+  </datafield>
+  <datafield tag="028" ind1="0" ind2="2">
+    <subfield code="a">HL50486260</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(cd.)</subfield>
+  </datafield>
+  <datafield tag="028" ind1="0" ind2="2">
+    <subfield code="a">63011108</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(diction coach 1)</subfield>
+  </datafield>
+  <datafield tag="028" ind1="0" ind2="2">
+    <subfield code="a">63011109</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(diction coach 2)</subfield>
+  </datafield>
+  <datafield tag="028" ind1="0" ind2="2">
+    <subfield code="a">63014792</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(CD 1)</subfield>
+  </datafield>
+  <datafield tag="028" ind1="0" ind2="2">
+    <subfield code="a">63014793</subfield>
+    <subfield code="b">Hal Leonard</subfield>
+    <subfield code="q">(CD 2)</subfield>
+  </datafield>
+  <datafield tag="035" ind1=" " ind2=" ">
+    <subfield code="a">(OCoLC)ocn826076986</subfield>
+  </datafield>
+  <datafield tag="035" ind1=" " ind2=" ">
+    <subfield code="a">(OCoLC)826076986</subfield>
+  </datafield>
+  <datafield tag="040" ind1=" " ind2=" ">
+    <subfield code="a">YDXCP</subfield>
+    <subfield code="b">eng</subfield>
+    <subfield code="e">rda</subfield>
+    <subfield code="c">YDXCP</subfield>
+    <subfield code="d">CLE</subfield>
+    <subfield code="d">NUI</subfield>
+    <subfield code="d">MYG</subfield>
+    <subfield code="d">DLC</subfield>
+  </datafield>
+  <datafield tag="041" ind1="0" ind2=" ">
+    <subfield code="a">ita</subfield>
+    <subfield code="a">ger</subfield>
+    <subfield code="a">fre</subfield>
+    <subfield code="a">eng</subfield>
+    <subfield code="e">ita</subfield>
+    <subfield code="e">ger</subfield>
+    <subfield code="e">fre</subfield>
+    <subfield code="e">eng</subfield>
+    <subfield code="g">eng</subfield>
+  </datafield>
+  <datafield tag="042" ind1=" " ind2=" ">
+    <subfield code="a">lccopycat</subfield>
+  </datafield>
+  <datafield tag="048" ind1=" " ind2=" ">
+    <subfield code="b">vf01</subfield>
+    <subfield code="a">ka01</subfield>
+  </datafield>
+  <datafield tag="050" ind1="0" ind2="0">
+    <subfield code="a">M1507.A+</subfield>
+  </datafield>
+  <datafield tag="100" ind1="0" ind2="0">
+    <subfield code="a">Pickins, Slim</subfield>
+    <subfield code="b">More Stuff</subfield>
+  </datafield>
+  <datafield tag="245" ind1="0" ind2="4">
+    <subfield code="a">The Arias for bass :</subfield>
+    <subfield code="b">complete package : with diction coach and accompaniment CDs /</subfield>
+    <subfield code="c">compiled and edited by Robert L. Larsen.</subfield>
+  </datafield>
+  <datafield tag="264" ind1=" " ind2="1">
+    <subfield code="a">New York, NY :</subfield>
+    <subfield code="b">G. Schirmer, Inc.,</subfield>
+    <subfield code="c">2013.</subfield>
+  </datafield>
+  <datafield tag="264" ind1=" " ind2="2">
+    <subfield code="a">Milwaukee, WI :</subfield>
+    <subfield code="b">Distributed by Hal Leonard Corporation</subfield>
+  </datafield>
+  <datafield tag="300" ind1=" " ind2=" ">
+    <subfield code="a">1 score (263 pages) ;</subfield>
+    <subfield code="c">31 cm +</subfield>
+    <subfield code="e">4 sound discs (digital ; 4 3/4 in.)</subfield>
+  </datafield>
+  <datafield tag="336" ind1=" " ind2=" ">
+    <subfield code="a">notated music</subfield>
+    <subfield code="b">ntm</subfield>
+    <subfield code="2">rdacontent</subfield>
+  </datafield>
+  <datafield tag="336" ind1=" " ind2=" ">
+    <subfield code="a">performed music</subfield>
+    <subfield code="b">prm</subfield>
+    <subfield code="2">rdacontent</subfield>
+  </datafield>
+  <datafield tag="337" ind1=" " ind2=" ">
+    <subfield code="a">unmediated</subfield>
+    <subfield code="b">n</subfield>
+    <subfield code="2">rdamedia</subfield>
+  </datafield>
+  <datafield tag="337" ind1=" " ind2=" ">
+    <subfield code="a">audio</subfield>
+    <subfield code="b">s</subfield>
+    <subfield code="2">rdamedia</subfield>
+  </datafield>
+  <datafield tag="338" ind1=" " ind2=" ">
+    <subfield code="a">volume</subfield>
+    <subfield code="b">nc</subfield>
+    <subfield code="2">rdacarrier</subfield>
+  </datafield>
+  <datafield tag="338" ind1=" " ind2=" ">
+    <subfield code="a">audio disc</subfield>
+    <subfield code="b">sd</subfield>
+    <subfield code="2">rdacarrier</subfield>
+  </datafield>
+  <datafield tag="490" ind1="1" ind2=" ">
+    <subfield code="a">G. Schirmer opera anthology</subfield>
+  </datafield>
+  <datafield tag="546" ind1=" " ind2=" ">
+    <subfield code="b">staff notation</subfield>
+  </datafield>
+  <datafield tag="546" ind1=" " ind2=" ">
+    <subfield code="a">Italian, French, German, and English words; non-English texts also printed with English translations.</subfield>
+  </datafield>
+  <datafield tag="500" ind1=" " ind2=" ">
+    <subfield code="a">Opera arias; acc. arr. for piano.</subfield>
+  </datafield>
+  <datafield tag="500" ind1=" " ind2=" ">
+    <subfield code="a">William Billingham, pianist on CDs.</subfield>
+  </datafield>
+  <datafield tag="500" ind1=" " ind2=" ">
+    <subfield code="a">disc 1-2 diction coach -- disc 3-4 accompaniment CDs.</subfield>
+  </datafield>
+  <datafield tag="505" ind1="0" ind2=" ">
+    <subfield code="a">Il barbiere di Siviglia. La calunnia / Gioachino Rossini -- La Boh&#xE8;&#x300;me. Vecchia zimarra, senti / Giacomo Puccini -- La Cenerentola. Miei rampolli femminini / Gioachino Rossini -- Don Giovanni. Madamina! Il catalogo &#xE8; questo / Wolfgang Amadeus Mozart -- Don Pasquale. Ah! Un foco insolito / Gaetano Donizetti -- Die Entf&#xFC;hrung aus dem Serail. O, wie will ich triumphiren / Wolfgang Amadeus Mozart  -- Ernani. Infelice! E tuo credevi / Giuseppe Verdi -- Eugene Onegin. Gremin's aria / Pyotr Il'yich Tchaikovsky -- Faust. Le veau d'or ; Vous qui faites l'endormie / Charles Gounod -- Der Freisch&#xFC;tz. Schweig'! Schweig'! Damit dich niemand warnt / Carl Maria von Weber -- Les huguenots. Pour le couvents c'est fini (Piff, paff) / Giacomo Meyerbeer -- La jolie fille de Perth. Quand la flamme de l'amour / Georges Bizet -- Lucia di Lammermoor. Dalle stanze ove Lucia / Gaetano Donizetti -- Die lustigen Weiber von Windsor. Als B&#xFC;blein klein / Otto Nicolai -- Macbeth. Come dal ciel precipita / Giuseppe Verdi -- Manon. &#xC9;pouse quelque brave fille / Jules Massenet -- The mother of us all. What what is it / Virgil Thomson -- Le nozze di Figaro. La vendetta ; Se vuol ballare ; Non pi&#xF9; andrai ; Aprite un po' quegl'occhi / Wolfgang Amadeus Mozart -- Simon Boccanegra. Il lacerato spirito / Giuseppe Verdi -- La sonnambula. Vi ravviso / Vincenzo Bellini -- Street scene. Let things be like they always was / Kurt Weill -- I vespri siciliani. O tu, Palermo / Giuseppe Verdi -- Die Zauberfl&#xF6;te. O Isis und Osiris ; In diesen heil'gen Hallen / Wolfgang Amadeus Mozart.</subfield>
+  </datafield>
+  <datafield tag="650" ind1=" " ind2="0">
+    <subfield code="a">Operas</subfield>
+    <subfield code="v">Excerpts</subfield>
+    <subfield code="v">Vocal scores with piano.</subfield>
+  </datafield>
+  <datafield tag="650" ind1=" " ind2="0">
+    <subfield code="a">Recorded accompaniments (Low voice)</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2=" ">
+    <subfield code="a">Larsen, Robert L.,</subfield>
+    <subfield code="d">1934-</subfield>
+    <subfield code="e">editor,</subfield>
+    <subfield code="e">compiler.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2=" ">
+    <subfield code="a">Billingham, William,</subfield>
+    <subfield code="e">performer.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Rossini, Gioacchino,</subfield>
+    <subfield code="d">1792-1868.</subfield>
+    <subfield code="t">Barbiere di Siviglia.</subfield>
+    <subfield code="p">Calunnia &#xE8; un venticello.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Puccini, Giacomo,</subfield>
+    <subfield code="d">1858-1924.</subfield>
+    <subfield code="t">Boh&#xE8;me.</subfield>
+    <subfield code="p">Vecchia zimarra.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Rossini, Gioacchino,</subfield>
+    <subfield code="d">1792-1868.</subfield>
+    <subfield code="t">Cenerentola.</subfield>
+    <subfield code="p">Miei rampolli femminini.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Don Giovanni.</subfield>
+    <subfield code="p">Madamina, il catalogo &#xE8; questo.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Donizetti, Gaetano,</subfield>
+    <subfield code="d">1797-1848.</subfield>
+    <subfield code="t">Don Pasquale.</subfield>
+    <subfield code="p">Foco insolito.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Entf&#xFC;hrung aus dem Serail.</subfield>
+    <subfield code="p">Ha! wie will ich triumphieren.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Verdi, Giuseppe,</subfield>
+    <subfield code="d">1813-1901.</subfield>
+    <subfield code="t">Ernani.</subfield>
+    <subfield code="p">Infelice! e tu credevi.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Tchaikovsky, Peter Ilich,</subfield>
+    <subfield code="d">1840-1893.</subfield>
+    <subfield code="t">Evgeni&#x12D; Onegin.</subfield>
+    <subfield code="p">Arii&#xFE20;a&#xFE21; kni&#xFE20;a&#xFE21;zi&#xFE20;a&#xFE21;.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Gounod, Charles,</subfield>
+    <subfield code="d">1818-1893.</subfield>
+    <subfield code="t">Faust.</subfield>
+    <subfield code="p">Veau d'or est toujours debout.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Gounod, Charles,</subfield>
+    <subfield code="d">1818-1893.</subfield>
+    <subfield code="t">Faust.</subfield>
+    <subfield code="p">Vous qui faites l'endormie.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Weber, Carl Maria von,</subfield>
+    <subfield code="d">1786-1826.</subfield>
+    <subfield code="t">Freisch&#xFC;tz.</subfield>
+    <subfield code="p">Schweig', schweig'! damit dich niemand warnt.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Meyerbeer, Giacomo,</subfield>
+    <subfield code="d">1791-1864.</subfield>
+    <subfield code="t">Huguenots.</subfield>
+    <subfield code="p">Piff, paff.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Bizet, Georges,</subfield>
+    <subfield code="d">1838-1875.</subfield>
+    <subfield code="t">Jolie fille de Perth.</subfield>
+    <subfield code="p">Quand la flamme de l'amour.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Donizetti, Gaetano,</subfield>
+    <subfield code="d">1797-1848.</subfield>
+    <subfield code="t">Lucia di Lammermoor.</subfield>
+    <subfield code="p">Dalle stanze ove Lucia.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Nicolai, Otto,</subfield>
+    <subfield code="d">1810-1849.</subfield>
+    <subfield code="t">Lustigen Weiber von Windsor.</subfield>
+    <subfield code="p">Als B&#xFC;blein klein.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Verdi, Giuseppe,</subfield>
+    <subfield code="d">1813-1901.</subfield>
+    <subfield code="t">Macbeth.</subfield>
+    <subfield code="p">Come dal ciel precipita.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Massenet, Jules,</subfield>
+    <subfield code="d">1842-1912.</subfield>
+    <subfield code="t">Manon.</subfield>
+    <subfield code="p">&#xC9;pouse quelque brave fille.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Thomson, Virgil,</subfield>
+    <subfield code="d">1896-1989.</subfield>
+    <subfield code="t">Mother of us all.</subfield>
+    <subfield code="p">What what is it.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Nozze di Figaro.</subfield>
+    <subfield code="p">Vendetta.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Nozze di Figaro.</subfield>
+    <subfield code="p">Se vuol ballare, signor contino.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Nozze di Figaro.</subfield>
+    <subfield code="p">Non pi&#xF9; andrai farfallone.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Nozze di Figaro.</subfield>
+    <subfield code="p">Aprite un po' quegl' occhi.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Verdi, Giuseppe,</subfield>
+    <subfield code="d">1813-1901.</subfield>
+    <subfield code="t">Simon Boccanegra.</subfield>
+    <subfield code="p">Lacerato spirito.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Bellini, Vincenzo,</subfield>
+    <subfield code="d">1801-1835.</subfield>
+    <subfield code="t">Sonnambula.</subfield>
+    <subfield code="p">Vi ravviso, o luoghi ameni.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Weill, Kurt,</subfield>
+    <subfield code="d">1900-1950.</subfield>
+    <subfield code="t">Street scene.</subfield>
+    <subfield code="p">Let things be like they always was.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Verdi, Giuseppe,</subfield>
+    <subfield code="d">1813-1901.</subfield>
+    <subfield code="t">V&#xEA;pres siciliennes.</subfield>
+    <subfield code="p">Et toi Palerme.</subfield>
+    <subfield code="l">Italian.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Zauberfl&#xF6;te.</subfield>
+    <subfield code="p">O Isis und Osiris (Aria and chorus)</subfield>
+    <subfield code="p">O Isis und Osiris.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="700" ind1="1" ind2="2">
+    <subfield code="i">Contains (expression):</subfield>
+    <subfield code="a">Mozart, Wolfgang Amadeus,</subfield>
+    <subfield code="d">1756-1791.</subfield>
+    <subfield code="t">Zauberfl&#xF6;te.</subfield>
+    <subfield code="p">In diesen heil'gen Hallen.</subfield>
+    <subfield code="s">Vocal score.</subfield>
+  </datafield>
+  <datafield tag="830" ind1=" " ind2="0">
+    <subfield code="a">G. Schirmer opera anthology.</subfield>
+  </datafield>
+  <datafield tag="906" ind1=" " ind2=" ">
+    <subfield code="a">0</subfield>
+    <subfield code="b">par</subfield>
+    <subfield code="c">copycat</subfield>
+    <subfield code="d">2</subfield>
+    <subfield code="e">ncip</subfield>
+    <subfield code="f">20</subfield>
+    <subfield code="g">y-genmusic</subfield>
+  </datafield>
+  <datafield tag="925" ind1="0" ind2=" ">
+    <subfield code="a">acquire</subfield>
+    <subfield code="b">2 shelf copies</subfield>
+    <subfield code="x">policy default</subfield>
+  </datafield>
+  <datafield tag="955" ind1=" " ind2=" ">
+    <subfield code="a">vl34 2014-01-28 z-client</subfield>
+    <subfield code="i">vl34 2014-01-28</subfield>
+    <subfield code="e">vl34 2014-01-28 4 sound disc to MBRS for shelf label</subfield>
+    <subfield code="t">vl34 2014-01-28 copy 2, 4 sound disc to MBRS for shelf label</subfield>
+  </datafield>
+  <datafield tag="901" ind1=" " ind2=" ">
+    <subfield code="a">233</subfield>
+    <subfield code="b">AUTOGEN</subfield>
+    <subfield code="c">233</subfield>
+    <subfield code="t">biblio</subfield>
+  </datafield>
+  <datafield tag="998" ind1=" " ind2=" ">
+    <subfield code="d">v</subfield>
+  </datafield>
+
+</record>
diff --git a/Open-ILS/examples/elastic/bib-248-marc.xml b/Open-ILS/examples/elastic/bib-248-marc.xml
new file mode 100644 (file)
index 0000000..a0efb65
--- /dev/null
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+<record xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.loc.gov/MARC21/slim" xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd">
+  <leader>00975pam a2200337 a 4500</leader>
+  <controlfield tag="001">248</controlfield>
+  <controlfield tag="003">CONS</controlfield>
+  <controlfield tag="005">20110823130500.0</controlfield>
+  <controlfield tag="008">110422s2011    nyu           000 1 eng  </controlfield>
+  <datafield tag="010" ind1=" " ind2=" ">
+    <subfield code="a">  2011015247</subfield>
+  </datafield>
+  <datafield tag="020" ind1=" " ind2=" ">
+    <subfield code="a">9780307887436 :</subfield>
+    <subfield code="c">$24.00</subfield>
+  </datafield>
+  <datafield tag="020" ind1=" " ind2=" ">
+    <subfield code="a">030788743X :</subfield>
+    <subfield code="c">$24.00</subfield>
+  </datafield>
+  <datafield tag="035" ind1=" " ind2=" ">
+    <subfield code="a">(DLC)  2011015247</subfield>
+  </datafield>
+  <datafield tag="040" ind1=" " ind2=" ">
+    <subfield code="a">DLC</subfield>
+    <subfield code="c">DLC</subfield>
+    <subfield code="d">NjBwBT</subfield>
+    <subfield code="d">GCmBT</subfield>
+  </datafield>
+  <datafield tag="042" ind1=" " ind2=" ">
+    <subfield code="a">pcc</subfield>
+  </datafield>
+  <datafield tag="050" ind1="0" ind2="0">
+    <subfield code="a">PS3603.L548</subfield>
+    <subfield code="b">R43 2011</subfield>
+  </datafield>
+  <datafield tag="082" ind1="0" ind2="0">
+    <subfield code="a">813/.6</subfield>
+    <subfield code="2">22</subfield>
+  </datafield>
+  <datafield tag="100" ind1="1" ind2=" ">
+    <subfield code="a">Cline, Ernest.</subfield>
+  </datafield>
+  <datafield tag="245" ind1="1" ind2="0">
+    <subfield code="a">Ready player one /</subfield>
+    <subfield code="c">Ernest Cline.</subfield>
+  </datafield>
+  <datafield tag="250" ind1=" " ind2=" ">
+    <subfield code="a">1st ed.</subfield>
+  </datafield>
+  <datafield tag="260" ind1=" " ind2=" ">
+    <subfield code="a">New York :</subfield>
+    <subfield code="b">Crown Publishers,</subfield>
+    <subfield code="c">c2011.</subfield>
+  </datafield>
+  <datafield tag="300" ind1=" " ind2=" ">
+    <subfield code="a">374 p. ;</subfield>
+    <subfield code="c">25 cm.</subfield>
+  </datafield>
+  <datafield tag="650" ind1=" " ind2="0">
+    <subfield code="a">Regression (Civilization)</subfield>
+    <subfield code="v">Fiction.</subfield>
+  </datafield>
+  <datafield tag="650" ind1=" " ind2="0">
+    <subfield code="a">Virtual reality</subfield>
+    <subfield code="v">Fiction.</subfield>
+  </datafield>
+  <datafield tag="650" ind1=" " ind2="0">
+    <subfield code="a">Utopias</subfield>
+    <subfield code="v">Fiction.</subfield>
+  </datafield>
+  <datafield tag="650" ind1=" " ind2="0">
+    <subfield code="a">Puzzles</subfield>
+    <subfield code="v">Fiction.</subfield>
+  </datafield>
+  <datafield tag="655" ind1=" " ind2="7">
+    <subfield code="a">Fantasy fiction.</subfield>
+    <subfield code="2">gsafd</subfield>
+  </datafield>
+  <datafield tag="850" ind1=" " ind2=" ">
+    <subfield code="b">1</subfield>
+  </datafield>
+  <datafield tag="901" ind1=" " ind2=" ">
+    <subfield code="a">248</subfield>
+    <subfield code="b">AUTOGEN</subfield>
+    <subfield code="c">248</subfield>
+    <subfield code="t">biblio</subfield>
+  </datafield>
+</record>
diff --git a/Open-ILS/examples/elastic/elastic-config.example.xml b/Open-ILS/examples/elastic/elastic-config.example.xml
new file mode 100644 (file)
index 0000000..84fbe98
--- /dev/null
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<elasticsearch xmlns="http://evergreen-ils.org/spec/elasticsearch/v1">
+
+  <index class="bib-search">
+
+    <transform>/openils/var/xsl/elastic-bib-transform.xsl</transform>
+    <number_of_shards>1</number_of_shards>
+
+    <fields>
+      <!-- 
+        Attributes:
+          search_group="string"   | optional; for grouped search fields (e.g. title)
+          name="string"           | required
+          search_field="boolean"  | defaults to false
+          facet_field="boolean"   | defaults to false
+          filter="boolean"        | defaults to false
+          sorter="boolean"        | defaults to false
+          weight="integer"        | defaults to 1
+      -->
+
+      <field search_group="title" name="maintitle" search_field="true" weight="10"/>
+      <field search_group="title" name="combined" search_field="true"/>
+      <field search_group="author" name="personal" search_field="true" weight="5"/>
+      <field search_group="author" name="combined" search_field="true" facet_field="true"/>
+      <field search_group="subject" name="combined" search_field="true" facet_field="true"/>
+      <field search_group="series" name="seriestitle" search_field="true" facet_field="true"/>
+      <field search_group="keyword" name="title" search_field="true" weight="10"/>
+      <field search_group="keyword" name="author" search_field="true" weight="5"/>
+      <field search_group="keyword" name="keyword" search_field="true"/>
+      <field search_group="keyword" name="publisher" search_field="true"/>
+      <field search_group="identifier" name="bibcn" search_field="true"/>
+      <field search_group="identifier" name="isbn" search_field="true"/>
+      <field search_group="identifier" name="issn" search_field="true"/>
+      <field search_group="identifier" name="lccn" search_field="true"/>
+      <field search_group="identifier" name="sudoc" search_field="true"/>
+      <field search_group="identifier" name="tech_number" search_field="true"/>
+      <field search_group="identifier" name="upc" search_field="true"/>
+      <field search_group="identifier" name="tcn" search_field="true"/>
+      <field search_group="identifier" name="bibid" search_field="true"/>
+
+      <!-- filters -->
+      <field name="mattype" filter="true"/>
+      <field name="audience" filter="true"/>
+      <field name="bib_level" filter="true"/>
+      <field name="date1" filter="true"/>
+      <field name="date2" filter="true"/>
+      <field name="item_form" filter="true"/>
+      <field name="item_lang" filter="true"/>
+      <field name="item_type" filter="true"/>
+      <field name="lit_form" filter="true"/>
+      <field name="search_format" filter="true"/>
+      <field name="sr_format" filter="true"/>
+      <field name="vr_format" filter="true"/>
+
+      <!-- sorters -->
+      <field name="authorsort" sorter="true"/>
+      <field name="titlesort" sorter="true"/>
+      <field name="pubdate" sorter="true"/>
+    </fields>
+  </index>
+</elasticsearch>
+
index 3fde569..a137db9 100644 (file)
@@ -19,7 +19,6 @@ use warnings;
 use OpenSRF::Utils::JSON;
 use OpenSRF::Utils::Logger qw/:logger/;
 use OpenILS::Utils::Fieldmapper;
-use OpenSRF::Utils::SettingsClient;
 use OpenILS::Utils::CStoreEditor q/:funcs/;
 use OpenILS::Elastic::BibSearch;
 use Digest::MD5 qw(md5_hex);
@@ -47,11 +46,15 @@ sub init {
     return if $init_done;
     $init_done = 1;
 
-    my $e = new_editor();
+    # NOTE: after things stabilize and maybe load balancing, etc. is
+    # tested and working, we could maintain a global $es so the 
+    # connection is cached instead of reconnecting on every search call.
+    my $es = OpenILS::Elastic::BibSearch->new;
+    $es->connect;
 
-    # no pkey
-    $bib_fields = $e->search_elastic_bib_field({name => {'!=' => undef}});
+    $bib_fields = $es->bib_fields;
 
+    my $e = new_editor();
     my $stats = $e->json_query({
         select => {ccs => ['id', 'opac_visible', 'is_available']},
         from => 'ccs',
@@ -99,9 +102,6 @@ __PACKAGE__->register_method(
             Org unit based item presence and availability filtering may
             optionally be added to the query.  See search options
             below.
-
-            See [ select * from elastic.bib_field where search_field; ] 
-            for full-text search fields and classes.
         /,
         params => [
             {   type => 'object',
@@ -208,9 +208,12 @@ sub bib_search {
         $elastic_query->{size} = 1000;
     }
 
-    my $es = OpenILS::Elastic::BibSearch->new('main');
-
+    # NOTE: after things stabilize and maybe load balancing, etc. is
+    # tested and working, we could maintain a global $es so the 
+    # connection is cached instead of reconnecting on every search call.
+    my $es = OpenILS::Elastic::BibSearch->new;
     $es->connect;
+
     my $results = $es->search($elastic_query);
 
     $logger->debug("ES elasticsearch returned: ".
@@ -278,20 +281,20 @@ sub compile_elastic_query {
 }
 
 # Format ES search aggregations to match the API response facet structure
-# {$cmf_id => {"Value" => $count}, $cmf_id2 => {"Value Two" => $count2}, ...}
+# {$field_id => {"Value" => $count}, $field_id2 => {"Value Two" => $count2}, ...}
 sub format_facets {
     my $aggregations = shift;
     my $facets = {}; 
 
     for my $fname (keys %$aggregations) {
 
-        my ($field_class, $name) = split(/\|/, $fname);
+        my ($search_group, $name) = split(/\|/, $fname);
 
         my ($bib_field) = grep {
-            $_->name eq $name && $_->search_group eq $field_class
+            $_->name eq $name && $_->search_group eq $search_group
         } @$bib_fields;
 
-        my $hash = $facets->{$bib_field->metabib_field} = {};
+        my $hash = $facets->{$bib_field->id} = {};
 
         my $values = $aggregations->{$fname}->{buckets};
         for my $bucket (@$values) {
@@ -305,7 +308,7 @@ sub format_facets {
 sub add_elastic_facet_aggregations {
     my ($elastic_query) = @_;
 
-    my @facet_fields = grep {$_->facet_field eq 't'} @$bib_fields;
+    my @facet_fields = grep {$_->facet_field} @$bib_fields;
     return unless @facet_fields;
 
     $elastic_query->{aggs} = {};
@@ -315,7 +318,7 @@ sub add_elastic_facet_aggregations {
         my $fgrp = $facet->search_group;
         $fname = "$fgrp|$fname" if $fgrp;
 
-        $elastic_query->{aggs}{$fname} = {terms => {field => "$fname.facet"}};
+        $elastic_query->{aggs}{$fname} = {terms => {field => "$fname|facet"}};
     }
 }
 
index 6ad3596..381a85d 100644 (file)
@@ -17,21 +17,25 @@ use strict;
 use warnings;
 use DBI;
 use Time::HiRes qw/time/;
-use OpenSRF::Utils::Logger qw/:logger/;
-use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use XML::LibXML;
+use XML::LibXML::XPathContext;
 use Search::Elasticsearch;
 use OpenSRF::Utils::JSON;
+use OpenSRF::Utils::Logger qw/:logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
 use Data::Dumper;
 $Data::Dumper::Indent = 0;
 
+# For parsing the Elasticsearch configuration file
+my $ES_NAMESPACE = 'http://evergreen-ils.org/spec/elasticsearch/v1';
+
 sub new {
-    my ($class, $cluster) = @_;
+    my ($class, %args) = @_;
 
-    my $self = {
-        cluster     => $cluster,
-        indices     => [],
-        marc_fields => []
-    };
+    my $self = {%args};
+
+    $self->{cluster} = 'main' unless $args{cluster};
 
     return bless($self, $class);
 }
@@ -48,7 +52,23 @@ sub nodes {
 
 sub indices {
     my $self = shift;
-    return $self->{indices};
+    return $self->{indices} if $self->{indices};
+
+    my $def;
+    eval { 
+        # All open indices
+        $def = $self->es->indices->get(
+            index => $self->index_class . '-*',
+            expand_wildcards => 'open'
+        );
+    };
+
+    if ($@) {
+        $logger->error("ES index lookup failed: $@");
+        return {};
+    }
+
+    return $self->{indices} = $def;
 }
 
 sub es {
@@ -57,7 +77,34 @@ sub es {
 }
 
 sub index_name {
-    die "Index name must be provided by sub-class\n";
+    my ($self) = @_;
+    return $self->{index_name};
+}
+
+my $xpc;
+sub xpath_context {
+    if (!$xpc) {
+        $xpc = XML::LibXML::XPathContext->new;                                    
+        $xpc->registerNs('es', $ES_NAMESPACE);
+    }
+    return $xpc;
+}
+
+# In maintenance mode we are working with specific indexes.
+# Otherwise all actions target the index alias which is index_class.
+sub index_target {
+    my ($self) = @_;
+    return $self->maintenance_mode ? $self->index_name : $self->index_class;
+}
+
+sub index_class {
+    die "index_class() should be implemented by sub-classes\n";
+}
+
+# Are we modifying indexes or just read/writing indexed data?
+sub maintenance_mode {
+    my $self = shift;
+    return $self->{maintenance_mode};
 }
 
 sub language_analyzers {
@@ -80,9 +127,9 @@ sub db {
     my $db_pass = $self->{db_pass};
     my $db_appn = $self->{db_appn} || 'Elastic Indexer';
 
-    # TODO Add application_name to dsn
+    my $dsn = 
+        "dbi:Pg:db=$db_name;host=$db_host;port=$db_port;application_name='$db_appn';";
 
-    my $dsn = "dbi:Pg:db=$db_name;host=$db_host;port=$db_port";
     $logger->debug("ES connecting to DB $dsn");
 
     $self->{db} = DBI->connect(
@@ -106,47 +153,212 @@ sub get_db_rows {
 
 # load the config via cstore.
 sub load_config {
-    my $self = shift;
+    my ($self) = @_;
+
     my $e = new_editor();
     my $cluster = $self->cluster;
 
-    $self->{nodes} = $e->search_elastic_node({cluster => $cluster, active => 't'});
+    my @nodes = $self->{nodes} ? @{$self->{nodes}} : ();
+
+    if (@nodes) {
+
+        $logger->info("ES overriding nodes with @nodes");
+        $self->{nodes} = \@nodes;
+
+    } else {
+
+        my %active = $self->maintenance_mode ? () : (active => 't');
+        my $nodes = $e->search_elastic_node({cluster => $cluster, %active});
+
+        $self->{nodes} = [
+            map {
+                sprintf("%s://%s:%d%s", $_->proto, $_->host, $_->port, $_->path)
+            } @$nodes
+        ];
+    }
 
     unless (@{$self->nodes}) {
         $logger->error("ES no nodes defined for cluster $cluster");
         return;
     }
 
-    $self->{indices} = $e->search_elastic_index({cluster => $cluster});
-
-    unless (@{$self->indices}) {
-        $logger->error("ES no indices defined for cluster $cluster");
+    if (!$self->index_class) {
+        $logger->error("ES index_class required to initialize");
         return;
     }
 }
 
-sub connect {
+sub load_es_config {
     my ($self) = @_;
-    $self->load_config;
 
-    my @nodes;
-    for my $server (@{$self->nodes}) {
-        push(@nodes, {
-            scheme => $server->proto,
-            host   => $server->host,
-            port   => $server->port,
-            path   => $server->path
-        });
+    my $cluster = $self->cluster;
+
+    if (!$self->indices || !keys(%{$self->indices})) {
+        $logger->info("ES no usable indices defined for cluster $cluster");
+        return unless $self->maintenance_mode;
     }
 
-    $logger->debug("ES connecting to ".scalar(@nodes)." nodes");
+    if (!$self->index_name) {
+        # Default to the index that has an alias matching our index_class
+        
+        for my $name (keys %{$self->indices}) {
+            if ($self->index_is_active($name)) {
+                $logger->info("ES defaulting to active index $name");
+                $self->{index_name} = $name;
+            }
+        }
+    }
+
+    # Load the main ES config file
+    
+    # TODO: 'dirs' option for 'conf'
+    #my $client = OpenSRF::Utils::SettingsClient->new;
+    #my $dir = $client->config_value("dirs", "conf");
 
-    eval { $self->{es} = Search::Elasticsearch->new(nodes => \@nodes) };
+    my $doc;
+    my $filename = $self->{es_config_file} 
+        || '/openils/conf/elastic-config.xml';
+
+    eval { $doc = XML::LibXML->load_xml(location => $filename) };
+
+    if ($@ || !$doc) {
+        my $msg = "ES could not parse elastic config file: $filename $@";
+        $logger->error($msg);
+        die "$msg\n";
+    }
+
+    $self->{es_config} = $doc->documentElement;
+}
+
+sub es_config {
+    my $self = shift;
+    return $self->{es_config};
+}
+
+sub active_index {
+    my $self = shift;
+    my $indices = $self->indices;
+    for my $name (keys %{$indices}) {
+        return $name if $self->index_is_active($name);
+    }
+    return undef;
+}
+
+# True if the named index has an alias matching our index class
+sub index_is_active {
+    my ($self, $name) = @_;
+
+    my $conf = $self->indices->{$name};
+    return 0 unless $conf;
+
+    my @aliases = keys %{$conf->{aliases}};
+    return 1 if grep {$_ eq $self->index_class} @aliases;
+
+    return 0;
+}
+
+
+sub index_config {
+    my $self = shift;
+    my $class = $self->index_class;
+
+    if (!$self->es_config) {
+        $logger->error("ES cannot load index config without a config file");
+        return undef;
+    }
+
+    my @conf;
+    eval {
+        @conf = $xpc->findnodes(
+            "//es:elasticsearch/es:index[\@class='$class']",
+            $self->es_config
+        );
+    };
+
+    if ($@ || !@conf) {
+        my $msg = "ES failed to locate config for index class '$class' $@";
+        $logger->error($msg);
+        die "$msg\n";
+    }
+
+    return $conf[0];
+}
+
+sub connect {
+    my ($self) = @_;
+
+    $self->load_config;
+
+    my @nodes = @{$self->nodes};
+    $logger->info("ES connecting to nodes: @nodes");
+
+    eval { 
+        $self->{es} = Search::Elasticsearch->new(
+            client => '6_0::Direct',
+            nodes  => \@nodes
+        );
+    };
 
     if ($@) {
         $logger->error("ES failed to connect to @nodes: $@");
         return;
     }
+
+    $self->load_es_config;
+}
+
+# Activates the currently loaded index while deactivating any active
+# index with the same cluster and index_class.
+# Applies an alias to the activated index equal to the index class.
+sub activate_index {
+    my ($self) = @_;
+
+    my $index = $self->index_name;
+
+    if (!$self->es->indices->exists(index => $index)) {
+        $logger->warn("ES cannot activate index '$index' which does not exist");
+        return;
+    }
+
+    my $from_index = $self->active_index;
+
+    # When activating an index, point the main alias toward the
+    # newly active index.
+    return $self->migrate_alias($self->index_class, $from_index, $index);
+}
+
+
+# Migrate an alias from one index to another.
+# If either from_index or to_index are not defined, then only half
+# of the migration (i.e. remove or add) is performed.
+sub migrate_alias {
+    my ($self, $alias, $from_index, $to_index) = @_;
+    return undef unless $alias && ($from_index || $to_index);
+
+    my @actions;
+
+    $from_index ||= '';
+    $to_index ||= '';
+    $logger->info("ES migrating alias [$alias] from $from_index to $to_index");
+
+    if ($from_index) {
+        push(@actions, {remove => {alias => $alias, index => $from_index}});
+    }
+
+    if ($to_index) {
+        push(@actions, {add => {alias => $alias, index => $to_index}});
+    }
+
+    eval {
+        $self->es->indices->update_aliases({body => {actions => \@actions}});
+    };
+
+    if ($@) {
+        $logger->error("ES alias migration [$alias] failed $@");
+        return undef;
+    } 
+
+    return 1;
 }
 
 sub delete_index {
@@ -163,6 +375,10 @@ sub delete_index {
         $logger->warn("ES index '$index' ".
             "does not exist in cluster '".$self->cluster."'");
     }
+
+    delete $self->indices->{$index};
+
+    return 1;
 }
 
 # Remove multiple documents from the index by ID.
@@ -176,7 +392,7 @@ sub delete_documents {
     eval {
     
         $result = $self->es->delete_by_query(
-            index => $self->index_name,
+            index => $self->index_target,
             type => 'record',
             body => {query => {terms => {_id => $ids}}}
         );
@@ -191,6 +407,30 @@ sub delete_documents {
     return $result;
 }
 
+# Returns true if a document with the requested ID exists.
+sub document_exists {
+    my ($self, $id) = @_;
+
+    my $result;
+
+    eval {
+        $result = $self->es->index(
+            index => $self->index_target,
+            type => 'record',
+            id => $id,
+        );
+    };
+
+
+    if ($@) {
+        $logger->error("ES document_exists failed with $@");
+        return undef;
+    } 
+
+    return $result ? 1 : 0;
+}
+
+# Create or replace a document.
 sub index_document {
     my ($self, $id, $body) = @_;
 
@@ -198,7 +438,7 @@ sub index_document {
 
     eval {
         $result = $self->es->index(
-            index => $self->index_name,
+            index => $self->index_target,
             type => 'record',
             id => $id,
             body => $body
@@ -219,6 +459,67 @@ sub index_document {
     return $result;
 }
 
+# Index a new document
+# This will fail if the document already exists.
+sub create_document {
+    my ($self, $id, $body) = @_;
+
+    my $result;
+
+    eval {
+        $result = $self->es->create(
+            index => $self->index_target,
+            type => 'record',
+            id => $id,
+            body => $body
+        );
+    };
+
+    if ($@) {
+        $logger->error("ES create_document failed with $@");
+        return undef;
+    } 
+
+    if ($result->{failed}) {
+        $logger->error("ES create document $id failed " . Dumper($result));
+        return undef;
+    }
+
+    $logger->debug("ES create => $id succeeded");
+    return $result;
+}
+
+
+# Partial document update
+# This will fail if the document does not exist.
+sub update_document {
+    my ($self, $id, $body) = @_;
+
+    my $result;
+
+    eval {
+        $result = $self->es->update(
+            index => $self->index_target,
+            type => 'record',
+            id => $id,
+            body => {doc => $body}
+        );
+    };
+
+    if ($@) {
+        $logger->error("ES update_document failed with $@");
+        return undef;
+    } 
+
+    if ($result->{failed}) {
+        $logger->error("ES update document $id failed " . Dumper($result));
+        return undef;
+    }
+
+    $logger->debug("ES update => $id succeeded");
+    return $result;
+}
+
 sub search {
     my ($self, $query) = @_;
 
@@ -230,7 +531,7 @@ sub search {
     eval {
         my $start_time = time;
         $result = $self->es->search(
-            index => $self->index_name,
+            index => $self->index_target,
             body => $query
         );
         $duration = time - $start_time;
@@ -254,13 +555,24 @@ sub search {
 # Avoid trying to index such data by lazily chopping it off
 # at 1/4 the limit to accomodate all UTF-8 chars.
 sub truncate_value {
-    my ($self, $value) = @_;
+    my ($self, $value, $length) = @_;
+    $length = 8190 unless $length;
     return substr($value, 0, 8190);
 }
 
 sub get_index_def {
-    my ($self) = @_;
-    return $self->es->indices->get(index => $self->index_name);
+    my ($self, $name) = @_;
+    $name ||= $self->index_name;
+
+    my $def;
+    eval { $def = $self->es->indices->get(index => $name) };
+
+    if ($@) {
+        $logger->error("ES cannot find index def for $name");
+        return undef;
+    }
+
+    return $def;
 }
 
 
index 1d5340d..d412e75 100644 (file)
@@ -1,6 +1,5 @@
-package OpenILS::Elastic::BibSearch;
 # ---------------------------------------------------------------
-# Copyright (C) 2019 King County Library System
+# Copyright (C) 2019-2020 King County Library System
 # Author: Bill Erickson <berickxx@gmail.com>
 #
 # This program is free software; you can redistribute it and/or
@@ -13,31 +12,97 @@ package OpenILS::Elastic::BibSearch;
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR code.  See the
 # GNU General Public License for more details.
 # ---------------------------------------------------------------
+package OpenILS::Elastic::BibField;
+use strict;
+use warnings;
+
+sub new {
+    my ($class, %args) = @_;
+    my $self = {%args};
+    return bless($self, $class);
+}
+sub id {
+    my $self = shift;
+    return $self->search_group ? 
+        $self->search_group . '|' . $self->name : $self->name;
+}
+sub search_group {
+    my $self = shift;
+    return $self->{search_group};
+}
+sub name {
+    my $self = shift;
+    return $self->{name};
+}
+sub search_field {
+    my $self = shift;
+    return $self->{search_field};
+}
+sub facet_field {
+    my $self = shift;
+    return $self->{facet_field};
+}
+sub weight {
+    my $self = shift;
+    return $self->{weight};
+}
+sub filter {
+    my $self = shift;
+    return $self->{filter};
+}
+sub sorter {
+    my $self = shift;
+    return $self->{sorter};
+}
+
+package OpenILS::Elastic::BibSearch;
 use strict;
 use warnings;
-use Encode;
 use DateTime;
 use Clone 'clone';
-use Business::ISBN;
-use Business::ISSN;
 use Time::HiRes qw/time/;
+use XML::LibXML;
+use XML::LibXML::XPathContext;
 use OpenSRF::Utils::Logger qw/:logger/;
 use OpenSRF::Utils::JSON;
+use OpenSRF::Utils::SettingsClient;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::DateTime qw/interval_to_seconds/;
 use OpenILS::Elastic;
+use OpenILS::Utils::Normalize;
 use base qw/OpenILS::Elastic/;
 
 # default number of bibs to index per batch.
 my $DEFAULT_BIB_BATCH_SIZE = 500;
+my $INDEX_CLASS = 'bib-search';
 
-my $INDEX_NAME = 'bib-search';
+# https://www.elastic.co/guide/en/elasticsearch/reference/current/ignore-above.html
+# Useful for ignoring excessively long filters and facets.
+# Only applied to the keyword variation of each index.  Does not affect
+# the 'text' varieties. The selected limit is arbitrary.
+my $IGNORE_ABOVE = 256;
+
+# Individual characters of some values like sorters provide less and less
+# value as the length of the text gets longer and longer.  Unlike
+# $IGNORE_ABOVE, this only trims the string, it does not prevent it from
+# getting indexed in the first place.  The selected limit is arbitrary.
+my $TRIM_ABOVE = 512;
 
 my $BASE_INDEX_SETTINGS = {
     analysis => {
         analyzer => {
             folding => {
-                filter => ['lowercase', 'asciifolding'],
+                filter => ['asciifolding', 'lowercase'],
+                tokenizer => 'standard'
+            },
+            icu_folding => {
+                filter => ['icu_folding', 'lowercase'],
+                tokenizer => 'icu_tokenizer'
+            },
+            stripapos => {
+                # "It's A Wonderful Live" => "Its A ..."
+                char_filter => ['stripapos'],
+                filter => ['lowercase'],
                 tokenizer => 'standard'
             },
             stripdots => {
@@ -60,6 +125,10 @@ my $BASE_INDEX_SETTINGS = {
             }
         },
         char_filter => {
+            stripapos => {
+                type => 'mapping',
+                mappings => ['\' =>']
+            },
             stripdots => {
                 type => 'mapping',
                 mappings => ['. =>']
@@ -72,7 +141,7 @@ my $BASE_INDEX_SETTINGS = {
     }
 };
 
-# Well-known bib-search index properties
+# Well-known bib-search index properties 
 my $BASE_PROPERTIES = {
     bib_source  => {type => 'integer'},
     create_date => {type => 'date'},
@@ -103,12 +172,13 @@ my $BASE_PROPERTIES = {
                 normalizer => 'custom_lowercase'
             },
             value => {
-                type => 'text',
+                type => 'keyword',
+                ignore_above => $IGNORE_ABOVE,
+                normalizer => 'custom_lowercase',
                 fields => {
-                    text_folded => {
-                        type => 'text',
-                        analyzer => 'folding'
-                    }
+                    text => {type => 'text'},
+                    text_folded => {type => 'text', analyzer => 'folding'},
+                    text_icu_folded => {type => 'text', analyzer => 'icu_folding'}
                 }
             }
         }
@@ -118,103 +188,232 @@ my $BASE_PROPERTIES = {
     # Values from grouped fields are copied into the group field.
     # Here we make some assumptions about the general purpose of
     # each group.
+    # The 'keyword' variation of each is used for exact matches, 
+    # starts with, and similar searches.
     # Note the ignore_above only affects the 'keyword' version of the
     # field, the assumption being text that large would solely be
     # searched via 'text' indexes.
     title => {
         type => 'keyword',
-        ignore_above => 256,
+        ignore_above => $IGNORE_ABOVE,
         normalizer => 'custom_lowercase',
         fields => {
             text => {type => 'text'},
             text_folded => {type => 'text', analyzer => 'folding'},
+            text_icu_folded => {type => 'text', analyzer => 'icu_folding'},
             text_spacedots => {type => 'text', analyzer => 'spacedots'},
-            text_stripdots => {type => 'text', analyzer => 'stripdots'}
+            text_stripdots => {type => 'text', analyzer => 'stripdots'},
+            text_stripapos => {type => 'text', analyzer => 'stripapos'}
         }
     },
     author => {
         type => 'keyword',
-        ignore_above => 256,
+        ignore_above => $IGNORE_ABOVE,
         normalizer => 'custom_lowercase',
         fields => {
             text => {type => 'text'},
             text_folded => {type => 'text', analyzer => 'folding'},
+            text_icu_folded => {type => 'text', analyzer => 'icu_folding'},
             text_spacedots => {type => 'text', analyzer => 'spacedots'},
-            text_stripdots => {type => 'text', analyzer => 'stripdots'}
+            text_stripdots => {type => 'text', analyzer => 'stripdots'},
+            text_stripapos => {type => 'text', analyzer => 'stripapos'}
         }
     },
     subject => {
         type => 'keyword',
-        ignore_above => 256,
+        ignore_above => $IGNORE_ABOVE,
         normalizer => 'custom_lowercase',
         fields => {
             text => {type => 'text'},
             text_folded => {type => 'text', analyzer => 'folding'},
+            text_icu_folded => {type => 'text', analyzer => 'icu_folding'},
             text_spacedots => {type => 'text', analyzer => 'spacedots'},
             text_stripdots => {type => 'text', analyzer => 'stripdots'}
         }
     },
     series => {
         type => 'keyword',
-        ignore_above => 256,
+        ignore_above => $IGNORE_ABOVE,
         normalizer => 'custom_lowercase',
         fields => {
             text => {type => 'text'},
             text_folded => {type => 'text', analyzer => 'folding'},
+            text_icu_folded => {type => 'text', analyzer => 'icu_folding'},
             text_spacedots => {type => 'text', analyzer => 'spacedots'},
-            text_stripdots => {type => 'text', analyzer => 'stripdots'}
+            text_stripdots => {type => 'text', analyzer => 'stripdots'},
+            text_stripapos => {type => 'text', analyzer => 'stripapos'}
         }
     },
     keyword => {
         # term (aka "keyword") searches are not used on the 
-        # keyword field, but we index it just the same (sans lowercase) 
-        # for structural consistency with other group fields.
+        # keyword field, but we structure the index just the same
+        # for consistency with other group fields.
         type => 'keyword',
-        ignore_above => 256,
+        ignore_above => 1, # essentially a no-op.
         fields => {
             text => {type => 'text'},
             text_folded => {type => 'text', analyzer => 'folding'},
+            text_icu_folded => {type => 'text', analyzer => 'icu_folding'},
             text_spacedots => {type => 'text', analyzer => 'spacedots'},
-            text_stripdots => {type => 'text', analyzer => 'stripdots'}
+            text_stripdots => {type => 'text', analyzer => 'stripdots'},
+            text_stripapos => {type => 'text', analyzer => 'stripapos'}
         }
     },
+    # Identifier fields only support 'keyword' indexes, no full-text.
     identifier => {
-        # Avoid full-text indexing on identifier fields.
         type => 'keyword',
-        ignore_above => 256,
+        ignore_above => $IGNORE_ABOVE,
         normalizer => 'custom_lowercase',
-    },
-
-    # Create some shortcut indexes for streamlining query_string searches.
-    ti => {type => 'text'},
-    au => {type => 'text'},
-    se => {type => 'text'},
-    su => {type => 'text'},
-    kw => {type => 'text'},
-    id => {
-        type => 'keyword',
-        ignore_above => 256
     }
 };
 
-my %SHORT_GROUP_MAP = (
-    title => 'ti',
-    author => 'au',
-    subject => 'su',
-    series => 'se',
-    keyword => 'kw',
-    identifier => 'id'
+# Map 'au' to 'author', etc.
+my %SEARCH_CLASS_ALIAS_MAP = (
+    ti => 'title.text',
+    au => 'author.text',
+    su => 'subject.text',
+    se => 'series.text',
+    kw => 'keyword.text',
+    pb => 'keyword|publisher.text',
+    id => 'identifier'
 );
 
-sub index_name {
-    return $INDEX_NAME;
+sub index_class {
+    return $INDEX_CLASS;
 }
 
-# TODO: add index-specific language analyzers to DB config
+# TODO: determine when/how to apply language analyzers.
+# e.g. create lang-specific index fields?
 sub language_analyzers {
     return ("english");
 }
 
+sub skip_holdings {
+    my $self = shift;
+    return $self->{skip_holdings};
+}
+
+sub bib_fields {
+    my $self = shift;
+    return $self->{bib_fields} if $self->{bib_fields};
+
+    my @bib_fields = $self->xpath_context->findnodes(
+        '//es:fields/es:field', $self->index_config);
+
+    my @fields;
+    for my $field (@bib_fields) {
+        
+        my %struct;
+
+        for my $key (qw/search_group name/) {
+            $struct{$key} = $field->getAttribute($key) || '';
+        }
+
+        for my $key (qw/search_field facet_field filter sorter/) {
+            $struct{$key} = ($field->getAttribute($key) || '') eq 'true';
+        }
+
+        push (@fields, OpenILS::Elastic::BibField->new(%struct));
+    }
+
+    return $self->{bib_fields} = \@fields;
+}
+
+sub xsl_file {
+    my ($self) = @_;
+
+    if (!$self->{xsl_file}) {
+        my @nodes = $self->xpath_context->findnodes(
+            '//es:transform/text()', $self->index_config);
+        $self->{xsl_file} = $nodes[0];
+    }
+
+    return $self->{xsl_file};
+}
+
+sub xsl_doc {
+    my ($self) = @_;
+
+    $self->{xsl_doc} = XML::LibXML->load_xml(location => $self->xsl_file)
+        unless $self->{xsl_doc};
+
+    return $self->{xsl_doc};
+}
+
+sub xsl_sheet {
+    my $self = shift;
+
+    $self->{xsl_sheet} = XML::LibXSLT->new->parse_stylesheet($self->xsl_doc)
+        unless $self->{xsl_sheet};
+
+    return $self->{xsl_sheet};
+}
+
+sub get_bib_data {
+    my ($self, $record_ids) = @_;
+
+    my $records = [];
+    my $db_data = $self->get_bib_db_data($record_ids);
+
+    for my $db_rec (@$db_data) {
+
+        my $rec = {fields => []};
+        push(@$records, $rec);
+
+        # Copy DB data into our record object.
+        $rec->{$_} = $db_rec->{$_} for 
+            qw/id bib_source metarecord create_date edit_date deleted/;
+
+        # No need to extract index values for delete records;
+        next if $rec->{deleted} == 1;
+
+        my $marc_doc = XML::LibXML->load_xml(string => $db_rec->{marc});
+        my $result = $self->xsl_sheet->transform($marc_doc);
+        my $output = $self->xsl_sheet->output_as_chars($result);
+
+        my @rows = split(/\n/, $output);
+        for my $row (@rows) {
+            my ($purpose, $search_group, $name, @tokens) = split(/ /, $row);
+
+            $search_group = '' if ($search_group || '') eq '_';
+
+            my $value = join(' ', @tokens);
+
+            my $field = {
+                purpose => $purpose,
+                search_group => $search_group,
+                name => $name,
+                value => $value
+            };
+
+            push(@{$rec->{fields}}, $field);
+        }
+    }
+
+    return $records;
+}
+
+sub get_bib_db_data {
+    my ($self, $record_ids) = @_;
+
+    my $ids_str = join(',', @$record_ids);
+
+    my $sql = <<SQL;
+SELECT DISTINCT ON (bre.id)
+    bre.id, 
+    bre.create_date, 
+    bre.edit_date, 
+    bre.source AS bib_source,
+    bre.deleted,
+    bre.marc
+FROM biblio.record_entry bre
+LEFT JOIN metabib.metarecord_source_map mmrsm ON (mmrsm.source = bre.id)
+WHERE bre.id IN ($ids_str)
+SQL
+
+    return $self->get_db_rows($sql);
+}
+
 sub create_index_properties {
     my ($self) = @_;
 
@@ -236,10 +435,8 @@ sub create_index_properties {
         } foreach qw/title subject series keyword/;
     }
 
-    # elastic.bib_field has no primary key field, so retrieve_all won't work.
-    # Note the name value may be repeated across search group depending
-    # on local configuration.
-    my $fields = new_editor()->search_elastic_bib_field({name => {'!=' => undef}});
+    # field_group will be undef for main/active fields
+    my $fields = $self->bib_fields;
 
     for my $field (@$fields) {
 
@@ -250,42 +447,60 @@ sub create_index_properties {
         my $def;
 
         if ($search_group) {
+            if ($field->search_field) {
 
-            # Use the same fields and analysis as the 'grouped' field.
-            $def = clone($properties->{$search_group});
-            $def->{copy_to} = [$search_group, $SHORT_GROUP_MAP{$search_group}];
+                # Use the same fields and analysis as the 'grouped' field.
+                $def = clone($properties->{$search_group});
 
-            # Apply ranking boost to each analysis variation.
-            my $flds = $def->{fields};
-            if ($flds && (my $boost = ($field->weight || 1)) > 1) {
-                $flds->{$_}->{boost} = $boost foreach keys %$flds;
+                # Copy grouped fields into their group parent field.
+                $def->{copy_to} = $search_group;
+
+                # Apply ranking boost to each analysis variation.
+                my $flds = $def->{fields};
+                if ($flds && (my $boost = ($field->weight || 1)) > 1) {
+                    $flds->{$_}->{boost} = $boost foreach keys %$flds;
+                }
             }
 
         } else {
-
-            # Non-grouped fields are used for filtering and sorting, so
-            # they don't need as much processing.
+            # Filters and sorters
 
             $def = {
                 type => 'keyword',
-                ignore_above => 256,
                 normalizer => 'custom_lowercase'
             };
+
+            # Long sorter values are not necessarily unexpected,
+            # e.g. long titles.
+            $def->{ignore_above} = $IGNORE_ABOVE unless $field->sorter;
+        }
+
+        if ($def) {
+            $logger->debug("ES adding field $field_name: ". 
+                OpenSRF::Utils::JSON->perl2JSON($def));
+    
+            $properties->{$field_name} = $def;
         }
 
-        if ($field->facet_field eq 't' && $def->{fields}) {
-            # Facet fields are used for aggregation which requires
-            # an additional unaltered keyword field.
-            $def->{fields}->{facet} = {
+        # Search and facet fields can have the same name/group pair,
+        # but are stored as separate fields in ES since the content
+        # may vary between the two.
+        if ($field->facet_field) {
+
+            # Facet fields are stored as separate fields, because their
+            # content may differ from the matching search field.
+            $field_name = "$field_name|facet";
+
+            $def = {
                 type => 'keyword',
-                ignore_above => 256
+                ignore_above => $IGNORE_ABOVE
             };
-        }
 
-        $logger->debug("ES adding field $field_name: ". 
-            OpenSRF::Utils::JSON->perl2JSON($def));
+            $logger->debug("ES adding field $field_name: ". 
+                OpenSRF::Utils::JSON->perl2JSON($def));
 
-        $properties->{$field_name} = $def;
+            $properties->{$field_name} = $def;
+        }
     }
 
     return $properties;
@@ -293,86 +508,114 @@ sub create_index_properties {
 
 sub create_index {
     my ($self) = @_;
+    my $index_name = $self->index_name;
 
-    if ($self->es->indices->exists(index => $INDEX_NAME)) {
-        $logger->warn("ES index '$INDEX_NAME' already exists");
+    if ($self->es->indices->exists(index => $index_name)) {
+        $logger->warn("ES index '$index_name' already exists in ES");
         return;
     }
 
     $logger->info(
-        "ES creating index '$INDEX_NAME' on cluster '".$self->cluster."'");
+        "ES creating index '$index_name' on cluster '".$self->cluster."'");
 
     my $properties = $self->create_index_properties;
 
     my $settings = $BASE_INDEX_SETTINGS;
-    $settings->{number_of_replicas} = scalar(@{$self->nodes});
-    $settings->{number_of_shards} = $self->index->num_shards;
+    $settings->{number_of_shards} = 1; # TODO $index_config->num_shards;
 
     my $conf = {
-        index => $INDEX_NAME,
+        index => $index_name,
         body => {settings => $settings}
     };
 
-    $logger->info("ES creating index '$INDEX_NAME'");
+    $logger->info("ES creating index '$index_name'");
 
     # Create the base index with settings
     eval { $self->es->indices->create($conf) };
 
     if ($@) {
-        $logger->error("ES failed to create index cluster=".  
-            $self->cluster. "index=$INDEX_NAME error=$@");
-        warn "$@\n\n";
-        return 0;
+        my $msg = "ES failed to create index cluster=".  
+            $self->cluster. "index=$index_name error=$@";
+
+        $logger->error($msg);
+        die "$msg\n";
     }
 
     # Create each mapping one at a time instead of en masse so we 
     # can more easily report when mapping creation fails.
-
     for my $field (keys %$properties) {
-        $logger->info("ES Creating index mapping for field $field");
-
-        eval { 
-            $self->es->indices->put_mapping({
-                index => $INDEX_NAME,
-                type  => 'record',
-                body  => {dynamic => 'strict', properties => {$field => $properties->{$field}}}
-            });
-        };
+        return 0 unless 
+            $self->create_one_field_index($field, $properties->{$field});
+    }
+
+    # Now that we've added the configured fields,
+    # add the shortened search_group aliases.
+    while (my ($alias, $field) = each %SEARCH_CLASS_ALIAS_MAP) {
 
-        if ($@) {
-            my $mapjson = OpenSRF::Utils::JSON->perl2JSON($properties->{$field});
+        return 0 unless $self->create_one_field_index(
+            $alias, {type => 'alias', path => $field});
+    }
 
-            $logger->error("ES failed to create index mapping: " .
-                "index=$INDEX_NAME field=$field error=$@ mapping=$mapjson");
+    return 1;
+}
 
-            warn "$@\n\n";
-            return 0;
-        }
+sub create_one_field_index {
+    my ($self, $field, $properties) = @_;
+
+    my $index_name = $self->index_name;
+
+    $logger->info("ES Creating index mapping for field $field");
+
+    eval { 
+        $self->es->indices->put_mapping({
+            index => $index_name,
+            type  => 'record',
+            body  => {
+                dynamic => 'strict', 
+                properties => {$field => $properties}
+            }
+        });
+    };
+
+    if ($@) {
+        my $mapjson = OpenSRF::Utils::JSON->perl2JSON($properties);
+
+        $logger->error("ES failed to create index mapping: " .
+            "index=$index_name field=$field error=$@ mapping=$mapjson");
+
+        warn "$@\n\n";
+        return 0;
     }
 
     return 1;
 }
 
-sub get_bib_data {
-    my ($self, $record_ids) = @_;
 
-    my $ids_str = join(',', @$record_ids);
+sub get_bib_field_for_data {
+    my ($self, $field) = @_;
 
-    my $sql = <<SQL;
-SELECT DISTINCT ON (bre.id, search_group, name, value)
-    bre.id, 
-    bre.create_date, 
-    bre.edit_date, 
-    bre.source AS bib_source,
-    bre.deleted,
-    mmrsm.metarecord,
-    (elastic.bib_record_properties(bre.id)).*
-FROM biblio.record_entry bre
-LEFT JOIN metabib.metarecord_source_map mmrsm ON (mmrsm.source = bre.id)
-WHERE bre.id IN ($ids_str)
-SQL
+    my @matches = grep {$_->name eq $field->{name}} @{$self->bib_fields};
 
-    return $self->get_db_rows($sql);
+    @matches = grep {
+        (($_->search_group || '') eq ($field->{search_group} || ''))
+    } @matches;
+
+    my ($match) = grep {
+        ($_->search_field && $field->{purpose} eq 'search') ||
+        ($_->facet_field && $field->{purpose} eq 'facet') ||
+        ($_->filter && $field->{purpose} eq 'filter') ||
+        ($_->sorter && $field->{purpose} eq 'sorter')
+    } @matches;
+
+    if (!$match) {
+        # Warning on mismatched fields can lead to a lot of logs
+        # while trying different field configs.  Consider a
+        # 'warn-on-field-mismatch' flag.
+        $logger->debug("ES No bib field matches extracted data ".
+            OpenSRF::Utils::JSON->perl2JSON($field));
+    }
+
+    return $match;
 }
 
 sub populate_bib_index_batch {
@@ -385,7 +628,7 @@ sub populate_bib_index_batch {
 
     $logger->info("ES indexing ".scalar(@$bib_ids)." records");
 
-    my $bib_data = $self->get_bib_data($bib_ids);
+    my $records = $self->get_bib_data($bib_ids);
 
     # Remove records that are marked deleted.
     # This should only happen when running in refresh mode.
@@ -394,9 +637,9 @@ sub populate_bib_index_batch {
     for my $bib_id (@$bib_ids) {
 
         # Every row in the result data contains the 'deleted' value.
-        my ($field) = grep {$_->{id} == $bib_id} @$bib_data;
+        my ($rec) = grep {$_->{id} == $bib_id} @$records;
 
-        if ($field->{deleted} == 1) { # not 't' / 'f'
+        if ($rec->{deleted} == 1) { # not 't' / 'f'
            $self->delete_documents($bib_id); 
         } else {
             push(@active_ids, $bib_id);
@@ -405,64 +648,123 @@ sub populate_bib_index_batch {
 
     $bib_ids = [@active_ids];
 
-    my $holdings = $self->load_holdings($bib_ids);
-    my $marc = $self->load_marc($bib_ids);
+    return 0 unless @$bib_ids;
+
+    my $holdings = $self->load_holdings($bib_ids) unless $self->skip_holdings;
 
     for my $bib_id (@$bib_ids) {
+        my ($rec) = grep {$_->{id} == $bib_id} @$records;
 
         my $body = {
-            marc => $marc->{$bib_id} || [],
-            holdings => $holdings->{$bib_id} || []
+            bib_source => $rec->{bib_source},
+            metarecord => $rec->{metarecord},
+            marc => []
         };
 
-        # there are multiple rows per bib in the data list.
-        my @fields = grep {$_->{id} == $bib_id} @$bib_data;
+        $body->{holdings} = $holdings->{$bib_id} || [] unless $self->skip_holdings;
 
-        my $first = 1;
-        for my $field (@fields) {
-        
-            if ($first) {
-                $first = 0;
-                # some values are repeated per field. 
-                # extract them from the first entry.
-                $body->{bib_source} = $field->{bib_source};
-                $body->{metarecord} = $field->{metarecord};
-
-                # ES ikes the "T" separator for ISO dates
-                ($body->{create_date} = $field->{create_date}) =~ s/ /T/g;
-                ($body->{edit_date} = $field->{edit_date}) =~ s/ /T/g;
-            }
+        # ES likes the "T" separator for ISO dates
+        ($body->{create_date} = $rec->{create_date}) =~ s/ /T/g;
+        ($body->{edit_date} = $rec->{edit_date}) =~ s/ /T/g;
 
+        for my $field (@{$rec->{fields}}) {
+            my $purpose = $field->{purpose};
             my $fclass = $field->{search_group};
             my $fname = $field->{name};
             my $value = $field->{value};
 
             next unless defined $value && $value ne '';
 
+            my $trim = $purpose eq 'sorter' ? $TRIM_ABOVE : undef;
+            $value = $self->truncate_value($value, $trim);
+
+            if ($purpose eq 'marc') {
+                # NOTE: we could create/require elastic.bib_field entries for 
+                # MARC values as well if we wanted to control the exact
+                # MARC data that's indexed.
+                $self->add_marc_value($body, $fclass, $fname, $value);
+                next;
+            }
+
+            # Ignore any data provided by the transform we have
+            # no configuration for.
+            next unless $self->get_bib_field_for_data($field);
+        
             $fname = "$fclass|$fname" if $fclass;
-            $value = $self->truncate_value($value);
+            $fname = "$fname|facet" if $purpose eq 'facet';
 
             if ($fname eq 'identifier|isbn') {
                 index_isbns($body, $value);
+
             } elsif ($fname eq 'identifier|issn') {
                 index_issns($body, $value);
+
+            } elsif ($fname eq 'pubdate') {
+                index_pubdate($body, $value);
+
+            } elsif ($fname =~ /sort/) {
+                index_sorter($body, $fname, $value);
+
             } else {
                 append_field_value($body, $fname, $value);
             }
         }
 
-        return 0 unless $self->index_document($bib_id, $body);
+        if ($self->skip_holdings) {
+            # Skip-Holdings mode performs an update for existing
+            # documents, so the attached holdings will remain, but 
+            # performs a create for documents that don't yet exist.
+            if ($self->document_exists($bib_id)) {
+                return 0 unless $self->update_document($bib_id, $body);
+            } else {
+                return 0 unless $self->create_document($bib_id, $body);
+            }
+        } else {
+            return 0 unless $self->index_document($bib_id, $body);
+        }
 
         $state->{start_record} = $bib_id + 1;
         $index_count++;
     }
 
-    $logger->info("ES indexing completed for records " . 
-        $bib_ids->[0] . '...' . $bib_ids->[-1]);
+    $self->{total_indexed} += $index_count;
+
+    $logger->info(sprintf(
+        "ES indexed %d records in this batch across records %d ... %d ".
+        "with a session total of %d",
+        $index_count, $bib_ids->[0], $bib_ids->[-1], $self->{total_indexed}));
+
+    my $batch_size = $state->{batch_size} || $DEFAULT_BIB_BATCH_SIZE;
 
     return $index_count;
 }
 
+sub index_sorter {
+    my ($body, $fname, $value) = @_;
+
+    $value = OpenILS::Utils::Normalize::search_normalize($value);
+
+    $value =~ s/^ +//g;
+
+    append_field_value($body, $fname, $value) if $value;
+}
+
+# Normalize the pubdate (used for sorting) to a single 4-digit year.
+# Pad with zeroes where the year fall short of 4 digits.
+sub index_pubdate {
+    my ($body, $value) = @_;
+
+    $value =~ s/\D//g;
+
+    return unless $value; # no numbers
+
+    $value = substr($value . '0' x 4, 0, 4);
+
+    return if $value eq '0000'; # treat as no date.
+
+    append_field_value($body, 'pubdate', $value) if $value;
+}
+
 
 # Indexes ISBN10, ISBN13, and formatted values of both (with hyphens)
 sub index_isbns {
@@ -470,25 +772,24 @@ sub index_isbns {
     return unless $value;
     
     my %seen; # deduplicate values
-
-    # Chop up the collected raw values into parts and let
-    # Business::* tell us which parts looks like ISBNs.
-    for my $token (split(/ /, $value)) {
-        if (length($token) > 8) {
-            my $isbn = Business::ISBN->new($token);
-            if ($isbn && $isbn->is_valid) {
-                if ($isbn->as_isbn10) {
-                    $seen{$isbn->as_isbn10->isbn} = 1;
-                    $seen{$isbn->as_isbn10->as_string} = 1;
-                }
-                if ($isbn->as_isbn13) {
-                    $seen{$isbn->as_isbn13->isbn} = 1;
-                    $seen{$isbn->as_isbn13->as_string} = 1;
-                }
-            }
+    my @values = OpenILS::Utils::Normalize::clean_isbns($value);
+    my $isbns = $values[0];
+    my $strings = $values[1];
+
+    for my $isbn (@$isbns) {
+        if ($isbn->as_isbn10) {
+            $seen{$isbn->as_isbn10->isbn} = 1; # compact
+            $seen{$isbn->as_isbn10->as_string} = 1; # with hyphens
+        }
+        if ($isbn->as_isbn13) {
+            $seen{$isbn->as_isbn13->isbn} = 1;
+            $seen{$isbn->as_isbn13->as_string} = 1;
         }
     }
 
+    # Add the unvalidated ISBNs
+    $seen{$_} = 1 for @$strings;
+
     append_field_value($body, 'identifier|isbn', $_) foreach keys %seen;
 }
 
@@ -498,17 +799,13 @@ sub index_issns {
     return unless $value;
 
     my %seen; # deduplicate values
+    my @issns = OpenILS::Utils::Normalize::clean_issns($value);
     
-    # Chop up the collected raw values into parts and let
-    # Business::* tell us which parts looks valid.
-    for my $token (split(/ /, $value)) {
-        my $issn = Business::ISSN->new($token);
-        if ($issn && $issn->is_valid) {
-            # no option in business::issn to get the unformatted value.
-            (my $unformatted = $issn->as_string) =~ s/-//g;
-            $seen{$unformatted} = 1;
-            $seen{$issn->as_string} = 1;
-        }
+    for my $issn (@issns) {
+        # no option in business::issn to get the unformatted value.
+        (my $unformatted = $issn->as_string) =~ s/-//g;
+        $seen{$unformatted} = 1;
+        $seen{$issn->as_string} = 1;
     }
 
     append_field_value($body, 'identifier|issn', $_) foreach keys %seen;
@@ -521,10 +818,12 @@ sub append_field_value {
         if (ref $body->{$fname}) {
             # Three or more values encountered for field.
             # Add to the list.
+            return if grep {$_ eq $value} @{$body->{$fname}}; # dupe
             push(@{$body->{$fname}}, $value);
         } else {
             # Second value encountered for field.
             # Upgrade to array storage.
+            return if $body->{$fname} eq $value; # dupe
             $body->{$fname} = [$body->{$fname}, $value];
         }
     } else {
@@ -579,89 +878,52 @@ SQL
     return $holdings;
 }
 
-sub load_marc {
-    my ($self, $bib_ids) = @_;
-
-    my $bib_ids_str = join(',', @$bib_ids);
-
-    my $marc_data = $self->get_db_rows(<<SQL);
-SELECT record, tag, subfield, value
-FROM metabib.real_full_rec
-WHERE record IN ($bib_ids_str)
-SQL
-
-    $logger->info("ES found ".scalar(@$marc_data).
-        " MARC rows for current record batch");
-
-    my $marc = {};
-    for my $row (@$marc_data) {
+sub add_marc_value {
+    my ($self, $rec, $tag, $subfield, $value) = @_;
 
-        my $value = $row->{value};
-        next unless defined $value && $value ne '';
+    # XSL uses '_' when no subfield is present (e.g. controlfields)
+    $subfield = undef if $subfield eq '_';
 
-        my $subfield = $row->{subfield};
-        my $rec_id = $row->{record};
-        delete $row->{record}; # avoid adding this to the index
+    my ($match) = grep {
+        $_->{tag} eq $tag &&
+        ($_->{subfield} || '') eq ($subfield || '')
+    } @{$rec->{marc}};
 
-        $row->{value} = $value = $self->truncate_value($value);
+    if ($match) {
+        if (ref $match->{value}) {
+            # 3rd or more instance of tag/subfield for this record.
 
-        $marc->{$rec_id} = [] unless $marc->{$rec_id};
-        delete $row->{subfield} unless defined $subfield;
-
-        # Add values to existing record/tag/subfield rows.
-  
-        my $existing;
-        for my $entry (@{$marc->{$rec_id}}) {
-            next unless $entry->{tag} eq $row->{tag};
-
-            if (defined $subfield) {
-                if (defined $entry->{subfield}) {
-                    if ($subfield eq $entry->{subfield}) {
-                        $existing = $entry;
-                        last;
-                    }
-                }
-            } elsif (!defined $entry->{subfield}) {
-                # Neither has a subfield value / not all tags have subfields
-                $existing = $entry;
-                last;
-            }
-        }
+            # avoid dupes
+            return if grep {$_ eq $value} @{$match->{value}};
 
-        if ($existing) {
-            
-            $existing->{value} = [$existing->{value}] unless ref $existing->{value};
-            push(@{$existing->{value}}, $value);
+            push(@{$match->{value}}, $value);
 
         } else {
+            # 2nd instance of tag/subfield for this record.
+            
+            # avoid dupes
+            return if $match->{value} eq $value;
 
-            push(@{$marc->{$rec_id}}, $row);
+            $match->{value} = [$match->{value}, $value];
         }
-    }
-
-    return $marc;
-}
 
+    } else {
+        # first instance of tag/subfield for this record.
 
+        $match = {tag => $tag, value => $value};
+        $match->{subfield} = $subfield if defined $subfield;
 
-sub index {
-    my $self = shift;
-    return $self->{index} if $self->{index};
-    ($self->{index}) = grep {$_->code eq $self->index_name} @{$self->indices};
-
-    $logger->error("No ndex configured named ".$self->index_name) unless $self->{index};
-
-    return $self->{index};
+        push(@{$rec->{marc}}, $match);
+    }
 }
 
-
 # Add data to the bib-search index
 sub populate_index {
     my ($self, $settings) = @_;
     $settings ||= {};
 
     my $index_count = 0;
-    my $total_indexed = 0;
+    $self->{total_indexed} = 0;
 
     # extract the database settings.
     for my $db_key (grep {$_ =~ /^db_/} keys %$settings) {
@@ -679,9 +941,6 @@ sub populate_index {
     while (1) {
 
         $index_count = $self->populate_bib_index_batch($settings);
-        $total_indexed += $index_count;
-
-        $logger->info("ES indexed $total_indexed bib records");
 
         # exit if we're only indexing a single record or if the 
         # batch indexer says there are no more records to index.
@@ -694,7 +953,7 @@ sub populate_index {
         }
     } 
 
-    $logger->info("ES bib indexing complete with $total_indexed records");
+    $logger->info("ES bib indexing complete with " . $self->{total_indexed} . " records");
 }
 
 sub get_bib_ids {
@@ -710,9 +969,10 @@ sub get_bib_ids {
 
     my ($select, $from, $where);
     if ($modified_since) {
+        $logger->info("ES bib indexing records modified since $modified_since");
         $select = "SELECT id";
-        $from   = "FROM elastic.bib_last_mod_date";
-        $where  = "WHERE last_mod_date > '$modified_since'";
+        $from   = "FROM elastic.bib_mod_since(QUOTE_LITERAL('$modified_since')::TIMESTAMPTZ)";
+        $where  = "WHERE TRUE";
     } else {
         $select = "SELECT id";
         $from   = "FROM biblio.record_entry";
index b492659..32c715e 100644 (file)
@@ -3,32 +3,6 @@ DROP SCHEMA IF EXISTS elastic CASCADE;
 
 BEGIN;
 
-ALTER TABLE config.record_attr_definition
-    ADD COLUMN elastic_field BOOLEAN NOT NULL DEFAULT FALSE;
-
-ALTER TABLE config.metabib_field
-    ADD COLUMN elastic_field BOOLEAN NOT NULL DEFAULT FALSE;
-
--- Provide a sweeping set of default elastic fields.
--- Likely this set of fields can be trimmed significantly for most sites,
--- since many of these fields will never be searched from the catalog.
--- Reducing the number of elastic_field's will improve indexing time, 
--- search time, and reduce Elastic disk space requirements.
-UPDATE config.record_attr_definition 
-    SET elastic_field = TRUE WHERE name NOT LIKE 'marc21_%';
-
-UPDATE config.metabib_field 
-    SET elastic_field = TRUE WHERE search_field OR facet_field;
-
-INSERT INTO config.global_flag (name, enabled, label) 
-VALUES (
-    'elastic.bib_search.enabled', FALSE,
-    'Elasticsearch Enable Bib Searching'
-), (
-    'elastic.bib_search.dynamic_properties', FALSE,
-    'Elasticsearch Dynamic Bib Record Properties'
-);
-
 CREATE SCHEMA elastic;
 
 CREATE TABLE elastic.cluster (
@@ -44,252 +18,55 @@ CREATE TABLE elastic.node (
     port    INTEGER NOT NULL,
     path    TEXT    NOT NULL DEFAULT '/',
     active  BOOLEAN NOT NULL DEFAULT FALSE,
-    cluster TEXT    NOT NULL 
+    cluster TEXT    NOT NULL
             REFERENCES elastic.cluster (code) ON DELETE CASCADE,
     CONSTRAINT node_once UNIQUE (host, port, path, cluster)
 );
 
-CREATE TABLE elastic.index (
-    id            SERIAL  PRIMARY KEY,
-    code          TEXT    NOT NULL, -- e.g. 'bib-search'
-    cluster       TEXT    NOT NULL 
-                  REFERENCES elastic.cluster (code) ON DELETE CASCADE,
-    active        BOOLEAN NOT NULL DEFAULT FALSE,
-    num_shards    INTEGER NOT NULL DEFAULT 1,
-    CONSTRAINT    index_type_once_per_cluster UNIQUE (code, cluster)
-);
-
-CREATE OR REPLACE VIEW elastic.bib_field AS
-    SELECT fields.* FROM (
-        SELECT 
-            NULL::INT AS metabib_field,
-            crad.name,
-            crad.label,
-            NULL AS search_group,
-            crad.sorter,
-            FALSE AS search_field,
-            FALSE AS facet_field,
-            1 AS weight
-        FROM config.record_attr_definition crad
-        WHERE crad.elastic_field
-        UNION
-        SELECT 
-            cmf.id AS metabib_field,
-            cmf.name,
-            cmf.label,
-            cmf.field_class AS search_group,
-            FALSE AS sorter,
-            -- always treat identifier fields as non-search fields.
-            (cmf.field_class <> 'identifier' AND cmf.search_field) AS search_field,
-            cmf.facet_field,
-            cmf.weight
-        FROM config.metabib_field cmf 
-        WHERE cmf.elastic_field
-    ) fields;
-
-
-CREATE OR REPLACE FUNCTION elastic.bib_record_attrs(bre_id BIGINT)
-RETURNS TABLE (
-    search_group TEXT,
-    name TEXT,
-    source BIGINT,
-    value TEXT
-)
-AS $FUNK$
-    SELECT DISTINCT record.* FROM (
-        SELECT 
-            NULL::TEXT AS search_group, 
-            crad.name, 
-            mrs.source, 
-            mrs.value
-        FROM metabib.record_sorter mrs
-        JOIN config.record_attr_definition crad ON (crad.name = mrs.attr)
-        WHERE mrs.source = $1 AND crad.elastic_field
-        UNION
-
-        -- record attributes
-        SELECT 
-            NULL::TEXT AS search_group, 
-            crad.name, 
-            mraf.id AS source, 
-            mraf.value
-        FROM metabib.record_attr_flat mraf
-        JOIN config.record_attr_definition crad ON (crad.name = mraf.attr)
-        WHERE mraf.id = $1 AND crad.elastic_field
-    ) record
-$FUNK$ LANGUAGE SQL STABLE;
-
-CREATE OR REPLACE FUNCTION elastic.bib_record_static_props(bre_id BIGINT)
-RETURNS TABLE (
-    search_group TEXT,
-    name TEXT,
-    source BIGINT,
-    value TEXT
-)
-AS $FUNK$
-    SELECT DISTINCT record.* FROM (
-        SELECT
-            cmf.field_class AS search_group, 
-            cmf.name, 
-            props.source, 
-            CASE WHEN cmf.joiner IS NOT NULL THEN
-                REGEXP_SPLIT_TO_TABLE(props.value, cmf.joiner)
-            ELSE
-                props.value
-            END AS value
-        FROM (
-            SELECT * FROM metabib.title_field_entry mtfe WHERE mtfe.source = $1
-            UNION 
-            SELECT * FROM metabib.author_field_entry mafe WHERE mafe.source = $1
-            UNION 
-            SELECT * FROM metabib.subject_field_entry msfe WHERE msfe.source = $1
-            UNION 
-            SELECT * FROM metabib.series_field_entry msrfe WHERE msrfe.source = $1
-            UNION 
-            SELECT * FROM metabib.keyword_field_entry mkfe WHERE mkfe.source = $1
-            UNION 
-            SELECT * FROM metabib.identifier_field_entry mife WHERE mife.source = $1
-        ) props
-        JOIN config.metabib_field cmf ON (cmf.id = props.field)
-        WHERE cmf.elastic_field
-    ) record
-$FUNK$ LANGUAGE SQL STABLE;
-
-CREATE OR REPLACE FUNCTION elastic.bib_record_dynamic_props(bre_id BIGINT)
-RETURNS TABLE (
-    search_group TEXT,
-    name TEXT,
-    source BIGINT,
-    value TEXT
-)
-AS $FUNK$
-    SELECT DISTINCT record.* FROM (
-        SELECT
-            cmf.field_class AS search_group, 
-            cmf.name, 
-            props.source, 
-            CASE WHEN cmf.joiner IS NOT NULL THEN
-                REGEXP_SPLIT_TO_TABLE(props.value, cmf.joiner)
-            ELSE
-                props.value
-            END AS value
-        FROM biblio.extract_metabib_field_entry(
-            $1, ' ', '{facet,search}',
-            (SELECT ARRAY_AGG(id) FROM config.metabib_field WHERE elastic_field)
-        ) props
-        JOIN config.metabib_field cmf ON (cmf.id = props.field)
-    ) record
-$FUNK$ LANGUAGE SQL STABLE;
-
-
-CREATE OR REPLACE FUNCTION elastic.bib_record_properties(bre_id BIGINT) 
-    RETURNS TABLE (
-        search_group TEXT,
-        name TEXT,
-        source BIGINT,
-        value TEXT
-    )
-    AS $FUNK$
-DECLARE
-    props_func TEXT;
-BEGIN
-
-    PERFORM 1 FROM config.internal_flag cif WHERE 
-        cif.name = 'elastic.bib_search.dynamic_properties' AND cif.enabled;
-
-    IF FOUND THEN
-        props_func := 'elastic.bib_record_dynamic_props';
-    ELSE
-        props_func := 'elastic.bib_record_static_props';
-    END IF;
-
-    RETURN QUERY EXECUTE $$
-        SELECT DISTINCT record.* FROM (
-            SELECT * FROM elastic.bib_record_attrs($$ || QUOTE_LITERAL(bre_id) || $$)
-            UNION
-            SELECT * FROM $$ || props_func || '(' || QUOTE_LITERAL(bre_id) || $$)
-        ) record
-    $$;
-END $FUNK$ LANGUAGE PLPGSQL;
-        
-/* give me bibs I should upate */
-
-CREATE OR REPLACE VIEW elastic.bib_last_mod_date AS
+CREATE OR REPLACE FUNCTION elastic.bib_mod_since(since TIMESTAMPTZ)
+    RETURNS TABLE (id BIGINT) AS $FUNK$
     /**
      * Last update date for each bib, which is taken from most recent
      * edit for either the bib, a linked call number, or a linked copy.
      * If no call numbers are linked, uses the bib edit date only.
      * Includes deleted data since it can impact indexing.
      */
-    WITH mod_dates AS (
-        SELECT bre.id, 
-            bre.edit_date, 
-            MAX(COALESCE(acn.edit_date, '1901-01-01')) AS max_call_number_edit_date, 
-            MAX(COALESCE(acp.edit_date, '1901-01-01')) AS max_copy_edit_date
-        FROM biblio.record_entry bre
-            LEFT JOIN asset.call_number acn ON (acn.record = bre.id)
-            LEFT JOIN asset.copy acp ON (acp.call_number = acn.id)
-        GROUP BY 1, 2
-    ) SELECT dates.id, 
-        GREATEST(dates.edit_date, 
-            GREATEST(dates.max_call_number_edit_date, dates.max_copy_edit_date)
-        ) AS last_mod_date
-    FROM mod_dates dates;
 
+    SELECT bre.id FROM biblio.record_entry bre WHERE bre.edit_date > since
+    UNION
 
-/* SEED DATA ------------------------------------------------------------ */
-
-INSERT INTO elastic.cluster (code, label) 
-    VALUES ('main', 'Main Cluster');
-
-INSERT INTO elastic.node (label, host, proto, port, active, cluster)
-    VALUES ('Localhost', 'localhost', 'http', 9200, TRUE, 'main');
+    SELECT bre.id
+        FROM biblio.record_entry bre 
+        JOIN asset.call_number acn ON acn.record = bre.id
+        WHERE acn.edit_date > since
+    UNION
 
-INSERT INTO elastic.index (code, active, cluster)
-    VALUES ('bib-search', TRUE, 'main');
+    SELECT bre.id
+        FROM biblio.record_entry bre 
+        JOIN asset.call_number acn ON acn.record = bre.id
+        JOIN asset.copy acp ON acp.call_number = acn.id
+        WHERE acp.edit_date > since
 
-COMMIT;
+$FUNK$ LANGUAGE SQL;
 
-/* UNDO
-
-DROP SCHEMA IF EXISTS elastic CASCADE;
+/* SEED DATA ------------------------------------------------------------ */
 
-ALTER TABLE config.record_attr_definition DROP COLUMN elastic_field;
 
-ALTER TABLE config.metabib_field DROP COLUMN elastic_field;
+INSERT INTO config.global_flag (name, enabled, label, value)
+VALUES (
+    'elastic.bib_search.enabled', FALSE,
+    'Elasticsearch Enable Bib Searching', NULL
+);
 
-DELETE FROM config.global_flag WHERE name ~ 'elastic.*';
+INSERT INTO elastic.cluster (code, label) VALUES ('main', 'Main Cluster');
 
-*/
+INSERT INTO elastic.node (label, host, proto, port, active, cluster)
+    VALUES ('Localhost', 'localhost', 'http', 9200, TRUE, 'main');
 
 /*
-
--- Sample narrower set of elastic fields to avoid duplication and 
--- indexing data that will presumably never be searched in the catalog.
-
-UPDATE config.metabib_field SET elastic_field = FALSE
-WHERE 
-    (field_class = 'keyword' AND name <> 'keyword') OR
-    (field_class = 'subject' AND name = 'complete') OR
-    (field_class = 'author'  AND name = 'first_author')
-;
-
-UPDATE config.record_attr_definition SET elastic_field = FALSE
-WHERE name NOT IN (
-    'authorsort',
-    'date1',
-    'date2',
-    'bib_level',
-    'icon_format',
-    'item_form',
-    'item_lang',
-    'item_type',
-    'lit_form',
-    'pubdate',
-    'search_format',
-    'titlesort',
-    'sr_format',
-    'vr_format'
-);
+-- turn it on for the UI
+UPDATE config.global_flag SET enabled = TRUE WHERE name = 'elastic.bib_search.enabled';
 
 */
+
+COMMIT;
index 05ebc6b..5abe39d 100755 (executable)
@@ -3,23 +3,34 @@ use strict;
 use warnings;
 use Getopt::Long;
 use OpenSRF::Utils::JSON;
+use OpenSRF::Utils::Logger qw/:logger/;
 use OpenILS::Utils::Fieldmapper;
 use OpenILS::Utils::CStoreEditor;
 use OpenILS::Elastic::BibSearch;
 
-my $help;
+my $lockfile;
+my $batch_size = 500;
 my $osrf_config = '/openils/conf/opensrf_core.xml';
 my $cluster = 'main';
+my @nodes;
+my $index_class = 'bib-search';
+my $bib_transform;
+my $es_config_file;
 my $create_index;
 my $delete_index;
-my $index_name = 'bib-search'; # only supported index at time of writing
+my $index_name;
+my $activate_index;
 my $populate;
 my $index_record;
 my $start_record;
 my $stop_record;
 my $modified_since;
 my $max_duration;
-my $batch_size = 500;
+my $skip_holdings;
+my $list_indices;
+my $no_opensrf;
+my $force;
+my $help;
 
 # Database settings read from ENV by default.
 my $db_host = $ENV{PGHOST} || 'localhost';
@@ -32,16 +43,26 @@ my $db_appn = 'Elastic Indexer';
 GetOptions(
     'help'              => \$help,
     'osrf-config=s'     => \$osrf_config,
+    'lockfile=s'        => \$lockfile,
     'cluster=s'         => \$cluster,
+    'node=s'            => \@nodes,
     'create-index'      => \$create_index,
     'delete-index'      => \$delete_index,
-    'index=s'           => \$index_name,
+    'index-name=s'      => \$index_name,
+    'index-class=s'     => \$index_class,
     'index-record=s'    => \$index_record,
+    'activate-index'    => \$activate_index,
     'start-record=s'    => \$start_record,
     'stop-record=s'     => \$stop_record,
     'modified-since=s'  => \$modified_since,
     'max-duration=s'    => \$max_duration,
     'batch-size=s'      => \$batch_size,
+    'bib-transform=s'   => \$bib_transform,
+    'es-config-file=s'  => \$es_config_file,
+    'skip-holdings'     => \$skip_holdings,
+    'list-indices'      => \$list_indices,
+    'no-opensrf'        => \$no_opensrf,
+    'force'             => \$force,
     'db-name=s'         => \$db_name,
     'db-host=s'         => \$db_host,
     'db-port=s'         => \$db_port,
@@ -51,16 +72,23 @@ GetOptions(
     'populate'          => \$populate
 ) || die "\nSee --help for more\n";
 
+$index_name = "$index_class-$index_name" if $index_name;
+
 sub help {
     print <<HELP;
         Synopsis:
             
-            $0 --delete-index --create-index --index bib-search --populate
+            $0 --index-class bib-search --index-name bib-search-take-2 \
+                --create-index --populate --activate-index
 
         Options:
 
             --osrf-config <file-path>
 
+            --lockfile <path-to-file>
+                Enables lock file controls over the process.  If unset,
+                no lock file is created.
+
             --db-name <$db_name>
             --db-host <$db_host>
             --db-port <$db_port>
@@ -70,13 +98,38 @@ sub help {
                 Database connection values.  This is the Evergreen database
                 where values should be extracted for elastic search indexing.
 
+                Beware that data loaded through Evergreen, e.g. elasticsearch
+                configuration data, will be loaded from the DB used by the
+                running Evergreen instance, regardless of these --db-*
+                settings.
+
                 Values default to their PG* environment variable equivalent.
 
+            --no-opensrf
+                Avoid connecting to OpenSRF.  Requires passing at least
+                one --node.
+
+            --bib-transform <path_to_file>
+                Override the configured global config value for
+                'elastic.bib_search.transform_file'
+
+            --es-config-file <path_to_file>
+                Override the default ES configuration XML file.
+
             --cluster <name>
                 Specify a cluster name.  Defaults to 'main'.
 
-            --index <name>
-                Specify an index name.  Defaults to 'bib-search'.
+            --node <URL> [repeatable]
+                Override the configured ES nodes.
+
+            --index-class <class>
+                Specifies which data set the current index manages (e.g. bib-search)
+                Must match a well-known index class with backing code.
+
+            --index-name <name>
+                The index name will be automatically prepended with the
+                index class. e.g. "my-index" becomes "bib-search-my-index"
+                on the backend for the "bib-search" index class.
 
             --delete-index
                 Delete the specified index and all of its data. 
@@ -84,6 +137,10 @@ sub help {
             --create-index
                 Create an index whose name equals --index-name.
 
+            --activate-index
+                Activate the selected index while deactivating all other
+                indexes of the same index_class and cluster.
+
             --batch-size <number>
                 Index at most this many records per batch.
                 Default is 500.
@@ -103,6 +160,11 @@ sub help {
                 at regular intervals to keep the ES-indexed data in sync 
                 with the EG data.
 
+            --skip-holdings
+                Bypass indexing the holdings data.  This is useful
+                when reindexing for configuration changes, where the
+                underlying holdings data has not changed.
+
             --max-duration <duration>
                 Stop indexing once the process has been running for this
                 amount of time.
@@ -112,47 +174,94 @@ sub help {
                 are provided (e.g. --index-start-record) then all 
                 applicable values will be indexed.
 
+            --list-indices
+                List all Elasticsearch indices represented in the 
+                Evergreen database.
+
+            --force
+                Force various actions.
+
 HELP
     exit(0);
 }
 
 help() if $help;
 
-# connect to osrf...
-OpenSRF::System->bootstrap_client(config_file => $osrf_config);
-Fieldmapper->import(
-    IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
-OpenILS::Utils::CStoreEditor::init();
+if ($lockfile) {
+
+    die "I seem to be running already. If not remove $lockfile, try again\n" 
+        if -e $lockfile;
+
+    open(LOCK, ">$lockfile") or die "Cannot open lock file: $lockfile : $@\n";
+    print LOCK $$ or die "Cannot write to lock file: $lockfile : $@\n";
+    close LOCK;
+}
+
+# We only need to connect to opensrf to look up the nodes in the database.
+# If the nodes are provided and --no-opensrf is set, avoid the connection
+# and log to stdout.
+if (@nodes && $no_opensrf) {
+    $logger->set_log_stdout(1);
+    $logger->set_log_level($logger->INFO);
+
+} else {
+    OpenSRF::System->bootstrap_client(config_file => $osrf_config);
+    Fieldmapper->import(
+        IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
+    OpenILS::Utils::CStoreEditor::init();
+}
 
 my $es;
 
-if ($index_name eq 'bib-search') {
-    $es = OpenILS::Elastic::BibSearch->new($cluster);
+if ($index_class eq 'bib-search') {
+    $es = OpenILS::Elastic::BibSearch->new(
+        db_name => $db_name,
+        db_host => $db_host,
+        db_port => $db_port,
+        db_user => $db_user,
+        db_pass => 'REDACTED',
+        db_appn => $db_appn,
+        cluster => $cluster, 
+        nodes => \@nodes,
+        xsl_file => $bib_transform,
+        index_name => $index_name,
+        maintenance_mode => 1,
+        es_config_file => $es_config_file,
+        skip_holdings => $skip_holdings
+    );
 }
 
 if (!$es) {
-    die "Unknown index type: $index_name\n";
+    die "Unknown index class: $index_class\n";
 }
 
 $es->connect;
 
 if ($delete_index) {
+
+    if (!$force) {
+        my $active = $es->active_index;
+        if ($active && $active eq $index_name) {
+            die "Index '$index_name' is active!  " . 
+                "Use --force to delete an active index.\n";
+        }
+    }
+
     $es->delete_index or die "Index delete failed.\n";
 }
 
 if ($create_index) {
+
+    if ($index_name eq $index_class) {
+        die "An index name cannot match its index_class [$index_class]\n";
+    }
+
     $es->create_index or die "Index create failed.\n";
 }
 
 if ($populate) {
 
     my $settings = {
-        db_name => $db_name,
-        db_host => $db_host,
-        db_port => $db_port,
-        db_user => $db_user,
-        db_pass => 'REDACTED',
-        db_appn => $db_appn,
         index_record   => $index_record,
         start_record   => $start_record,
         stop_record    => $stop_record,
@@ -170,4 +279,31 @@ if ($populate) {
     $es->populate_index($settings) or die "Index populate failed.\n";
 }
 
+if ($activate_index) {
+    $es->activate_index or die "Index activation failed.\n";
+}
+
+if ($list_indices) {
+    my $indices = $es->indices;
+
+    for my $name (keys %{$indices}) {
+        my $index_def = $indices->{$name};
+
+        my @aliases;
+        if ($index_def) {
+            @aliases = keys(%{$index_def->{$name}->{aliases}});
+        } else {
+            warn "ES has no index named $name\n";
+        }
+
+        print sprintf(
+            "index_class=%s index_name=%s active=%s aliases=@aliases\n",
+            $es->index_class, $name, 
+            $es->index_is_active($name) ? 'yes' : 'no');
+    }
+}
+
+unlink $lockfile if $lockfile;
+
+
 
index c45e7c4..1202ce4 100755 (executable)
@@ -14,7 +14,8 @@ binmode(STDOUT, ':utf8');
 my $help;
 my $osrf_config = '/openils/conf/opensrf_core.xml';
 my $cluster = 'main';
-my $index = 'bib-search';
+my $index_class = 'bib-search';
+my $index_name;
 my $quiet = 0;
 my $query_string;
 
@@ -22,6 +23,7 @@ GetOptions(
     'help'              => \$help,
     'osrf-config=s'     => \$osrf_config,
     'cluster=s'         => \$cluster,
+    'index-name=s'      => \$index_name,
     'quiet'             => \$quiet,
 ) || die "\nSee --help for more\n";
 
@@ -29,10 +31,13 @@ sub help {
     print <<HELP;
         Synopsis:
 
-            $0
+            $0 --index-name <name>
 
         Performs a series of canned bib record searches
 
+        Note if --index-name is omitted, the currently active index on 
+        the 'bib-search' index class will be used.
+
 HELP
     exit(0);
 }
@@ -117,9 +122,16 @@ Fieldmapper->import(
     IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
 OpenILS::Utils::CStoreEditor::init();
 
-my $es = OpenILS::Elastic::BibSearch->new($cluster);
+my $es = OpenILS::Elastic::BibSearch->new(index_name => $index_name);
 $es->connect;
 
+if ($es->index_name) {
+    print "Using bib-search index '" . $es->index_name . "'\n";
+} else {
+    die "No active 'bib-search' index found.  ".
+        "Use --index-name or activate an index in the database.\n";
+}
+
 print "Searching...\n";
 for my $query_part (@$queries) {
 
index 297b92f..47c72da 100755 (executable)
@@ -14,7 +14,10 @@ binmode(STDOUT, ':utf8');
 my $help;
 my $osrf_config = '/openils/conf/opensrf_core.xml';
 my $cluster = 'main';
-my $index = 'bib-search';
+my @nodes;
+my $index_class = 'bib-search';
+my $index_name;
+my $field_group;
 my $quiet = 0;
 my $query_string;
 
@@ -22,6 +25,9 @@ GetOptions(
     'help'              => \$help,
     'osrf-config=s'     => \$osrf_config,
     'cluster=s'         => \$cluster,
+    'node=s'            => \@nodes,
+    'index-class=s'     => \$index_class,
+    'index-name=s'      => \$index_name,
     'quiet'             => \$quiet,
 ) || die "\nSee --help for more\n";
 
@@ -29,10 +35,13 @@ sub help {
     print <<HELP;
         Synopsis:
 
-            $0
+            $0 --index-name <name>
 
         Performs query string searches.
 
+        Note if --index-name is omitted, the currently active index on 
+        the 'bib-search' index class will be used.
+
 HELP
     exit(0);
 }
@@ -46,9 +55,22 @@ Fieldmapper->import(
     IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
 OpenILS::Utils::CStoreEditor::init();
 
-my $es = OpenILS::Elastic::BibSearch->new($cluster);
+my $es = OpenILS::Elastic::BibSearch->new(
+    maintenance_mode => 1, # allows access to inactive indexes
+    cluster => $cluster, 
+    nodes => \@nodes,
+    field_group => $field_group,
+    index_name => $index_name
+);
 $es->connect;
 
+if ($es->index_name) {
+    print "Using bib-search index '" . $es->index_name . "'\n";
+} else {
+    die "No active 'bib-search' index found.  ".
+        "Use --index-name or activate an index in the database.\n";
+}
+
 print <<MESSAGE;
 
 Enter a query string to perform a search. Ctrl-c to exit.
@@ -73,7 +95,7 @@ while (1) {
     next unless $query_string;
 
     my $query = {
-        _source => ['id', 'title|maintitle'] , # return only a few fields
+        _source => ['id', 'title|maintitle', 'author|personal'] , # return only a few fields
         from => 0,
         size => 10,
         sort => [{'_score' => 'desc'}],
@@ -86,6 +108,17 @@ while (1) {
                 # Search the base keyword text index by default.
                 default_field => 'keyword.text'
             } 
+        },
+        # Request highligh data for title/author text fields.
+        # See below for logging highlight response data.
+        highlight => {
+            # Pre/Post tags modified to match stock Evergreen.
+            pre_tags => '<b class="oils_SH">',
+            post_tags => '</b>',
+            fields => {
+                'title*.text' => {},
+                'author*.text' => {}
+            }
         }
     };
 
@@ -107,6 +140,12 @@ while (1) {
             $hit->{_id}, $hit->{_score}, 
             ($hit->{_source}->{'title|maintitle'} || '')
         );
+
+# Uncomment to log highlighted field data.
+#        for my $hl (keys %{$hit->{highlight}}) {
+#            my @values = @{$hit->{highlight}->{$hl}};
+#            print "\tHighlight: $hl => @values\n";
+#        }
     }
 }
 
diff --git a/Open-ILS/xsl/elastic-bib-transform.xsl b/Open-ILS/xsl/elastic-bib-transform.xsl
new file mode 100644 (file)
index 0000000..a6d974c
--- /dev/null
@@ -0,0 +1,1339 @@
+<xsl:stylesheet
+  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+  xmlns:marc="http://www.loc.gov/MARC21/slim"
+  version="1.0">
+  <xsl:output encoding="UTF-8" method="text"/>
+
+  <!--
+      Prints one index value per line for data found by transforming
+      a MARCXML record.
+
+      Output:
+
+      $index_purpose $index_class $index_name $value
+
+      - $value is the only string in the output that may contain spaces.
+
+      e.g.
+
+      search subject topic South America
+      facet author personal Janey Jam "Jojo" Jones
+  -->
+
+  <xsl:template match="@*|node()">
+    <xsl:call-template name="compile_searches" />
+    <xsl:call-template name="compile_facets" />
+    <xsl:call-template name="compile_filters" />
+    <xsl:call-template name="compile_sorters" />
+    <xsl:call-template name="compile_marc" />
+  </xsl:template>
+
+  <xsl:template name="compile_searches">
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">650</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was topic -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">651</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was geographic -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">avxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">655</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was genre -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">630</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was uniftitle -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">adfgklmnoprstvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">600</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was name -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdfgjklmnopqrstuvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">610</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was corpname -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdfgklmnoprstuvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">611</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was meeting -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">acdefgjklnpqstuvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">490</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">800</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">tflmnoprs</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">810</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">tflmnoprs</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">830</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">adfgklmnoprst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">100</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <xsl:with-param name="index_name">personal</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">100</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was personal -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">110</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdn</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">111</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was meeting -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">acdegng</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">700</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was added_personal -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">710</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was coorporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">711</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was meeting -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">acde</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">400</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was added_personal -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcd</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">410</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcd</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">411</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was meeting -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">acdegq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">010</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">lccn</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">010</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">lccn</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">020</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">isbn</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">020</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">isbn</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">022</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">issn</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">022</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">issn</xsl:with-param>
+      <xsl:with-param name="index_subfields">y</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">022</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">issn</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">024</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">upc</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">024</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">upc</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">027</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">tech_number</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">027</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">tech_number</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">028</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">tech_number</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">074</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">sudoc</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">074</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">sudoc</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">086</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">sudoc</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">086</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">sudoc</xsl:with-param>
+      <xsl:with-param name="index_subfields">z</xsl:with-param>
+    </xsl:call-template>
+    <!-- NOTE bibcn depends on local values for asset.call_number_class -->
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">092</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">bibcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">099</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">bibcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">086</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">bibcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">092</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">bibcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">099</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">bibcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">086</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">bibcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">901</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">bibid</xsl:with-param>
+      <xsl:with-param name="index_subfields">c</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">901</xsl:with-param>
+      <xsl:with-param name="field_class">identifier</xsl:with-param>
+      <xsl:with-param name="index_name">tcn</xsl:with-param>
+      <xsl:with-param name="index_subfields">c</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">130</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was uniform -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">210</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was abbreviated -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefghijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">222</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was magazine -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">240</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was uniform -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">245</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <xsl:with-param name="index_name">maintitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">245</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was maintitle -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">245</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was proper -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abefgijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">245</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was responsibility -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">c</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">246</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was alternative -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgjklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">247</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was former -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">260</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">publisher</xsl:with-param>
+      <xsl:with-param name="index_subfields">b</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">264</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">publisher</xsl:with-param>
+      <xsl:with-param name="index_subfields">b</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">245</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">title</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">100</xsl:with-param>
+      <xsl:with-param name="field_class">keyword</xsl:with-param>
+      <xsl:with-param name="index_name">author</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">400</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">ptv</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">410</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcde</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">410</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">ptv</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">411</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was conference -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">acdegq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">411</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was seriestitle -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">ptv</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">440</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefghijklmnopqrstuvwyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">490</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefghijklmnopqrstuvwyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">490</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was uniform -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefghijklmnopqrstuvwyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">694</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">700</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was added -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">fgklmnoprst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">710</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was added -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">fgklmnoprst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">711</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was added -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">fklnpst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">730</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was added -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgjklmnopqrstuvwyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">740</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was added -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgijklmnopqrstuvwyz</xsl:with-param>
+    </xsl:call-template>
+
+    <!-- HERE HERE EREHE -->
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">780</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was previous -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">st</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">785</xsl:with-param>
+      <xsl:with-param name="field_class">title</xsl:with-param>
+      <!-- was succeeding -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">st</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">800</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was personal_series -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">800</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">fgklmnoprst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">810</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate_series -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdn</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">810</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcdn</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">811</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was conference_series -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="index_subfields">acdegnq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">811</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">fklnpstv</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_search_entry">
+      <xsl:with-param name="tag">830</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="index_subfields">abcefgijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="keyword_full_entry" />
+  </xsl:template>
+
+  <xsl:template name="compile_facets">
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">650</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was topic -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcdvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">651</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was geographic -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">avxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">600</xsl:with-param>
+      <xsl:with-param name="field_class">subject</xsl:with-param>
+      <!-- was name -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcdfgjklmnopqrstuvxyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">490</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">a</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">800</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">tflmnoprs</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">810</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">tflmnoprs</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">830</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">adfgklmnoprst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">100</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was personal -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcdq</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">110</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">710</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">ab</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">410</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcd</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">400</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields"></xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">410</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was corporate -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields"></xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">410</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields"></xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">411</xsl:with-param>
+      <xsl:with-param name="field_class">author</xsl:with-param>
+      <!-- was conference -->
+      <xsl:with-param name="index_name">combined</xsl:with-param>
+      <xsl:with-param name="facet_subfields"></xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">440</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcefghijklmnopqrstuvwyz</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">490</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields"></xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">694</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields"></xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">800</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">fgklmnoprst</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">810</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcdn</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">811</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">fklnpstv</xsl:with-param>
+    </xsl:call-template>
+    <xsl:call-template name="add_facet_entry">
+      <xsl:with-param name="tag">830</xsl:with-param>
+      <xsl:with-param name="field_class">series</xsl:with-param>
+      <xsl:with-param name="index_name">seriestitle</xsl:with-param>
+      <xsl:with-param name="facet_subfields">abcefgijklmnopqrstuvwxyz</xsl:with-param>
+    </xsl:call-template>
+  </xsl:template>
+
+  <xsl:template name="compile_sorters">
+
+    <!-- author sort is the first 1XX value -->
+    <xsl:for-each select="marc:datafield[starts-with(@tag, '1')]">
+      <xsl:sort select="@tag"/>
+      <xsl:if test="position() = 1">
+        <xsl:call-template name="add_sorter_entry">
+          <xsl:with-param name="name">authorsort</xsl:with-param>
+          <xsl:with-param name="value">
+            <xsl:call-template name="subfieldSelect"></xsl:call-template>
+          </xsl:with-param>
+        </xsl:call-template>
+      </xsl:if>
+    </xsl:for-each>
+
+    <!-- title sort is the 245a non-filing -->
+    <xsl:for-each select="marc:datafield[@tag='245']">
+      <!-- 245 is non-repeating but it happens.  just take the first value -->
+      <xsl:if test="position() = 1">
+        <xsl:variable name="full_title">
+          <xsl:call-template name="subfieldSelect">
+            <xsl:with-param name="codes">a</xsl:with-param>
+          </xsl:call-template>
+        </xsl:variable>
+        <xsl:variable name="offset">
+          <xsl:choose>
+            <xsl:when test="number(@ind2) = @ind2">
+              <xsl:value-of select="@ind2" />
+            </xsl:when>
+            <xsl:otherwise>
+              <xsl:text>0</xsl:text>
+            </xsl:otherwise>
+          </xsl:choose>
+        </xsl:variable>
+        <xsl:call-template name="add_sorter_entry">
+          <xsl:with-param name="name">titlesort</xsl:with-param>
+          <xsl:with-param name="value" select="substring($full_title, $offset + 1)" />
+        </xsl:call-template>
+      </xsl:if>
+    </xsl:for-each>
+
+    <!-- pubdate is the same as the date1 filter -->
+    <xsl:call-template name="add_sorter_entry">
+      <xsl:with-param name="name">pubdate</xsl:with-param>
+      <xsl:with-param name="value">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">008</xsl:with-param>
+          <xsl:with-param name="offset">7</xsl:with-param>
+          <xsl:with-param name="length">4</xsl:with-param>
+        </xsl:call-template>
+      </xsl:with-param>
+    </xsl:call-template>
+
+  </xsl:template>
+
+  <xsl:template name="compile_filters">
+
+    <!-- start with filters that are not used within composite filters.
+         These can be added to the document inline. -->
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">date1</xsl:with-param>
+      <xsl:with-param name="value">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">008</xsl:with-param>
+          <xsl:with-param name="offset">7</xsl:with-param>
+          <xsl:with-param name="length">4</xsl:with-param>
+        </xsl:call-template>
+      </xsl:with-param>
+      <xsl:with-param name="default_value">0000</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">date2</xsl:with-param>
+      <xsl:with-param name="value">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">008</xsl:with-param>
+          <xsl:with-param name="offset">11</xsl:with-param>
+          <xsl:with-param name="length">4</xsl:with-param>
+        </xsl:call-template>
+      </xsl:with-param>
+      <xsl:with-param name="default_value">9999</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">lit_form</xsl:with-param>
+      <xsl:with-param name="value">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">008</xsl:with-param>
+          <xsl:with-param name="offset">33</xsl:with-param>
+          <xsl:with-param name="length">1</xsl:with-param>
+        </xsl:call-template>
+      </xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">item_lang</xsl:with-param>
+      <xsl:with-param name="value">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">008</xsl:with-param>
+          <xsl:with-param name="offset">35</xsl:with-param>
+          <xsl:with-param name="length">3</xsl:with-param>
+        </xsl:call-template>
+      </xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">audience</xsl:with-param>
+      <xsl:with-param name="value">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">008</xsl:with-param>
+          <xsl:with-param name="offset">22</xsl:with-param>
+          <xsl:with-param name="length">1</xsl:with-param>
+        </xsl:call-template>
+      </xsl:with-param>
+    </xsl:call-template>
+
+    <!-- Filters that may be used within composite filters are
+         stored in a local variable so they can first be added
+         to the document, then used to compile composite filters -->
+
+    <xsl:variable name="item_type">
+      <xsl:call-template name="leader_value">
+        <xsl:with-param name="offset">6</xsl:with-param>
+        <xsl:with-param name="length">1</xsl:with-param>
+      </xsl:call-template>
+    </xsl:variable>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">item_type</xsl:with-param>
+      <xsl:with-param name="value" select="$item_type" />
+    </xsl:call-template>
+
+    <xsl:variable name="bib_level">
+      <xsl:call-template name="leader_value">
+        <xsl:with-param name="offset">7</xsl:with-param>
+        <xsl:with-param name="length">1</xsl:with-param>
+      </xsl:call-template>
+    </xsl:variable>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">bib_level</xsl:with-param>
+      <xsl:with-param name="value" select="$bib_level" />
+    </xsl:call-template>
+
+    <xsl:variable name="item_form">
+      <xsl:call-template name="controlfield_value">
+        <xsl:with-param name="tag">008</xsl:with-param>
+        <xsl:with-param name="offset">23</xsl:with-param>
+        <xsl:with-param name="length">1</xsl:with-param>
+      </xsl:call-template>
+    </xsl:variable>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">item_form</xsl:with-param>
+      <xsl:with-param name="value" select="$item_form" />
+    </xsl:call-template>
+
+    <xsl:variable name="category_of_material">
+      <xsl:call-template name="controlfield_value">
+        <xsl:with-param name="tag">007</xsl:with-param>
+        <xsl:with-param name="offset">0</xsl:with-param>
+        <xsl:with-param name="length">1</xsl:with-param>
+      </xsl:call-template>
+    </xsl:variable>
+
+    <xsl:variable name="vr_format">
+      <xsl:if test="$category_of_material = 'v'">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">007</xsl:with-param>
+          <xsl:with-param name="offset">4</xsl:with-param>
+          <xsl:with-param name="length">1</xsl:with-param>
+        </xsl:call-template>
+      </xsl:if>
+    </xsl:variable>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">vr_format</xsl:with-param>
+      <xsl:with-param name="value" select="$vr_format" />
+    </xsl:call-template>
+
+    <xsl:variable name="sr_format">
+      <xsl:if test="$category_of_material = 's'">
+        <xsl:call-template name="controlfield_value">
+          <xsl:with-param name="tag">007</xsl:with-param>
+          <xsl:with-param name="offset">3</xsl:with-param>
+          <xsl:with-param name="length">1</xsl:with-param>
+        </xsl:call-template>
+      </xsl:if>
+    </xsl:variable>
+
+    <xsl:call-template name="add_filter_entry">
+      <xsl:with-param name="name">sr_format</xsl:with-param>
+      <xsl:with-param name="value" select="$sr_format" />
+    </xsl:call-template>
+
+    <!-- use the extracted raw filters to create composite filters -->
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">blu-ray</xsl:with-param>
+      <xsl:with-param name="vr_format" select="$vr_format" />
+      <xsl:with-param name="vr_format_codes">s</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">book</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">at</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_type" />
+      <xsl:with-param name="item_form_not_codes">abcfoqrs</xsl:with-param>
+      <xsl:with-param name="bib_level" select="$bib_level" />
+      <xsl:with-param name="bib_level_codes">acdm</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">braille</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">a</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">f</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">casaudiobook</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">i</xsl:with-param>
+      <xsl:with-param name="sr_format" select="$sr_format" />
+      <xsl:with-param name="sr_format_codes">l</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">casmusic</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">j</xsl:with-param>
+      <xsl:with-param name="sr_format" select="$sr_format" />
+      <xsl:with-param name="sr_format_codes">l</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">cdaudiobook</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">i</xsl:with-param>
+      <xsl:with-param name="sr_format" select="$sr_format" />
+      <xsl:with-param name="sr_format_codes">f</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">cdaudiobook</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">j</xsl:with-param>
+      <xsl:with-param name="sr_format" select="$sr_format" />
+      <xsl:with-param name="sr_format_codes">f</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">dvd</xsl:with-param>
+      <xsl:with-param name="vr_format" select="$vr_format" />
+      <xsl:with-param name="vr_format_codes">v</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">eaudio</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">i</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">oqs</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">ebook</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">at</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">oqs</xsl:with-param>
+      <xsl:with-param name="bib_level" select="$bib_level" />
+      <xsl:with-param name="bib_level_codes">acdm</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">electronic</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">os</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">equip</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">r</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">evideo</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">g</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">oqs</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">kit</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">op</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">lpbook</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">at</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">d</xsl:with-param>
+      <xsl:with-param name="bib_level" select="$bib_level" />
+      <xsl:with-param name="bib_level_codes">acdm</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">map</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">ef</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">microform</xsl:with-param>
+      <xsl:with-param name="item_form" select="$item_form" />
+      <xsl:with-param name="item_form_codes">abc</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">music</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">j</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">phonomusic</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">j</xsl:with-param>
+      <xsl:with-param name="sr_format" select="$sr_format" />
+      <xsl:with-param name="sr_format_codes">abcde</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">phonospoken</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">i</xsl:with-param>
+      <xsl:with-param name="sr_format" select="$sr_format" />
+      <xsl:with-param name="sr_format_codes">abcde</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">picture</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">k</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">serial</xsl:with-param>
+      <xsl:with-param name="bib_level" select="$bib_level" />
+      <xsl:with-param name="bib_level_codes">bs</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">score</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">cd</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">software</xsl:with-param>
+      <xsl:with-param name="item_type" select="$item_type" />
+      <xsl:with-param name="item_type_codes">m</xsl:with-param>
+    </xsl:call-template>
+
+    <xsl:call-template name="add_composite_filter_entry">
+      <xsl:with-param name="name">search_format</xsl:with-param>
+      <xsl:with-param name="value">vhs</xsl:with-param>
+      <xsl:with-param name="vr_format" select="$vr_format" />
+      <xsl:with-param name="vr_format_codes">b</xsl:with-param>
+    </xsl:call-template>
+    
+  </xsl:template>
+
+  <xsl:template name="add_sorter_entry">
+    <xsl:param name="name"/>
+    <xsl:param name="value"/>
+    <xsl:text>sorter _ </xsl:text>
+    <xsl:value-of select="$name" />
+    <xsl:text> </xsl:text>
+    <xsl:value-of select="$value" />
+    <xsl:text>&#xa;</xsl:text>
+  </xsl:template>
+
+  <xsl:template name="add_filter_entry">
+    <xsl:param name="name"/>
+    <xsl:param name="value"/>
+    <xsl:param name="default_value"/>
+    <xsl:text>filter _ </xsl:text>
+    <xsl:value-of select="$name" />
+    <xsl:text> </xsl:text>
+    <xsl:choose>
+      <xsl:when test="$default_value and translate($value, ' ', '') = ''">
+        <xsl:value-of select="$default_value" />
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:value-of select="$value" />
+      </xsl:otherwise>
+    </xsl:choose>
+    <xsl:text>&#xa;</xsl:text>
+  </xsl:template>
+
+  <xsl:template name="add_composite_filter_entry">
+    <xsl:param name="name"/>
+    <xsl:param name="value"/>
+    <xsl:param name="item_type"/>
+    <xsl:param name="item_type_codes"/>
+    <xsl:param name="item_form"/>
+    <xsl:param name="item_form_codes"/>
+    <xsl:param name="item_form_not_codes"/>
+    <xsl:param name="bib_level"/>
+    <xsl:param name="bib_level_codes"/>
+    <xsl:param name="vr_format"/>
+    <xsl:param name="vr_format_codes"/>
+    <xsl:param name="sr_format"/>
+    <xsl:param name="sr_format_codes"/>
+
+    <xsl:variable name="item_type_matches" select="
+      not($item_type_codes) or (
+        $item_type != '' and
+        contains($item_type_codes, $item_type)
+      )
+    "/>
+
+    <xsl:variable name="item_form_matches" select="
+      (
+        not($item_form_codes) or
+        contains($item_form_codes, $item_form)
+      ) and (
+        not($item_form_not_codes) or
+        not(contains($item_form_not_codes, $item_form))
+      )
+    "/>
+
+    <xsl:variable name="bib_level_matches" select="
+      not($bib_level_codes) or (
+        $bib_level != '' and
+        contains($bib_level_codes, $bib_level)
+      )
+    "/>
+
+    <xsl:variable name="vr_format_matches" select="
+      not($vr_format_codes) or (
+        $vr_format != '' and
+        contains($vr_format_codes, $vr_format)
+      )
+    "/>
+
+    <xsl:variable name="sr_format_matches" select="
+      not($sr_format_codes) or (
+        $sr_format != '' and
+        contains($sr_format_codes, $sr_format)
+      )
+    "/>
+
+    <xsl:if test="
+        $item_type_matches and
+        $item_form_matches and
+        $bib_level_matches and
+        $sr_format_matches and
+        $vr_format_matches">
+      <xsl:call-template name="add_filter_entry">
+        <xsl:with-param name="name" select="$name" />
+        <xsl:with-param name="value" select="$value" />
+      </xsl:call-template>
+    </xsl:if>
+  </xsl:template>
+
+  <xsl:template name="leader_value">
+    <xsl:param name="offset" /> <!-- zero-based -->
+    <xsl:param name="length" />
+    <xsl:for-each select="marc:leader">
+      <xsl:value-of select="substring(text(), $offset + 1, $length)"/>
+    </xsl:for-each>
+  </xsl:template>
+
+  <xsl:template name="controlfield_value">
+    <xsl:param name="tag" />
+    <xsl:param name="offset" /> <!-- zero-based -->
+    <xsl:param name="length" />
+    <xsl:for-each select="marc:controlfield[@tag=$tag]">
+      <xsl:value-of select="substring(text(), $offset + 1, $length)"/>
+    </xsl:for-each>
+  </xsl:template>
+
+  <!-- Produces a single value for the specific tab/subfields.
+       Should only be used in cases where a single value is expected. -->
+  <xsl:template name="datafield_value">
+    <xsl:param name="tag" />
+    <xsl:param name="subfields" />
+    <xsl:for-each select="marc:datafield[@tag=$tag]">
+      <xsl:call-template name="subfieldSelect">
+        <xsl:with-param name="codes">
+          <xsl:value-of select="$subfields" />
+        </xsl:with-param>
+      </xsl:call-template>
+      <xsl:text> </xsl:text>
+    </xsl:for-each>
+  </xsl:template>
+
+  <xsl:template name="subfieldSelect">
+    <xsl:param name="codes">abcdefghijklmnopqrstuvwxyz</xsl:param>
+    <xsl:param name="delimeter">
+      <xsl:text> </xsl:text>
+    </xsl:param>
+    <xsl:variable name="str">
+      <xsl:for-each select="marc:subfield">
+        <xsl:if test="contains($codes, @code)">
+          <xsl:value-of select="text()"/>
+          <xsl:value-of select="$delimeter"/>
+        </xsl:if>
+      </xsl:for-each>
+    </xsl:variable>
+    <xsl:value-of select="substring($str,1,string-length($str)-string-length($delimeter))"/>
+  </xsl:template>
+
+  <xsl:template name="add_search_entry">
+    <xsl:param name="tag" />
+    <xsl:param name="field_class" />
+    <xsl:param name="index_name" />
+    <xsl:param name="index_subfields" />
+    <xsl:for-each select="marc:datafield[@tag=$tag] |
+      marc:datafield[@tag='880']/marc:subfield[@code='6'][starts-with(., $tag)]/..">
+      <xsl:text>search </xsl:text>
+      <xsl:value-of select="$field_class" /><xsl:text> </xsl:text>
+      <xsl:value-of select="$index_name" /><xsl:text> </xsl:text>
+      <xsl:call-template name="subfieldSelect">
+        <xsl:with-param name="codes">
+          <xsl:value-of select="$index_subfields" />
+        </xsl:with-param>
+      </xsl:call-template>
+      <xsl:text>&#xa;</xsl:text><!-- newline -->
+    </xsl:for-each>
+  </xsl:template>
+
+  <xsl:template name="add_facet_entry">
+    <xsl:param name="tag" />
+    <xsl:param name="field_class" />
+    <xsl:param name="index_name" />
+    <xsl:param name="facet_subfields" />
+    <xsl:for-each select="marc:datafield[@tag=$tag] |
+      marc:datafield[@tag='880']/marc:subfield[@code='6'][starts-with(., $tag)]/..">
+      <xsl:text>facet </xsl:text>
+      <xsl:value-of select="$field_class"/><xsl:text> </xsl:text>
+      <xsl:value-of select="$index_name"/><xsl:text> </xsl:text>
+      <xsl:call-template name="subfieldSelect">
+        <xsl:with-param name="codes">
+          <xsl:value-of select="$facet_subfields" />
+        </xsl:with-param>
+      </xsl:call-template>
+      <xsl:text>&#xa;</xsl:text><!-- newline -->
+    </xsl:for-each>
+  </xsl:template>
+
+  <!-- Dumps practically the entire document into a single
+       keyword|keyword index.
+  -->
+  <xsl:template name="keyword_full_entry">
+    <xsl:text>search keyword keyword </xsl:text>
+    <xsl:for-each select="marc:datafield">
+      <xsl:call-template name="subfieldSelect" />
+      <xsl:text> </xsl:text>
+    </xsl:for-each>
+    <xsl:text>&#xa;</xsl:text><!-- newline -->
+  </xsl:template>
+
+  <!-- print: marc $tag $subfield $value -->
+  <xsl:template name="compile_marc">
+    <xsl:for-each select="marc:leader">
+      <xsl:text>marc LDR _ </xsl:text>
+      <xsl:value-of select="text()"/>
+    </xsl:for-each>
+    <xsl:for-each select="marc:controlfield">
+      <xsl:text>marc </xsl:text>
+      <xsl:value-of select="@tag" />
+      <xsl:text> _ </xsl:text>
+      <xsl:value-of select="text()"/>
+      <xsl:text>&#xa;</xsl:text><!-- newline -->
+    </xsl:for-each>
+    <xsl:for-each select="marc:datafield">
+      <xsl:variable name="tag" select="@tag" />
+      <xsl:for-each select="marc:subfield">
+        <xsl:text>marc </xsl:text>
+        <xsl:value-of select="$tag" />
+        <xsl:text> </xsl:text>
+        <xsl:value-of select="@code" />
+        <xsl:text> </xsl:text>
+        <xsl:value-of select="text()"/>
+        <xsl:text>&#xa;</xsl:text><!-- newline -->
+      </xsl:for-each>
+    </xsl:for-each>
+  </xsl:template>
+
+
+</xsl:stylesheet>
+
+