Catalog Search Application

Academic Catalog Search Application Manual

This manual outlines the setup and operation of the enhanced Academic Catalog Search R Shiny application.

Part 1: Technical Implementation

1. Prerequisites & Setup

The application requires R (v4.0+) and the following R packages for core functionality:


Package

Purpose

shiny

Core web framework.

httr

Handling SRU API calls.

jsonlite, xml2/rvest

Data parsing (JSON/XML MARC records).

readr, stringr, dplyr

Data handling (CSV export, ISBN cleaning, batch results).

Place all UI and server logic into a single file named app.R within its project directory.

2. API Endpoints & Deployment

Ensure these SRU endpoints are correctly configured in your app.R code:

  • Library of Congress (LoC): https://lcweb2.loc.gov/zhf/srw/

  • German National Library (DNB): https://services.dnb.de/sru/dnb

  • WorldCat/OCLC: Requires institutional access/key.

  • British Library (BL): Endpoint must be determined and configured by the deployer.

The application can be deployed locally via RStudio's "Run App" or on a server (Shiny Server, RStudio Connect, or ShinyApps.io). Security Note: The server's firewall must permit outbound HTTPS traffic to all configured catalog endpoints.

Part 2: User Guide

The interface is divided into Configuration (left panel) and Search Results (main panel).

1. Search Configuration & Execution

Use the left panel to define search parameters, then click "Q Search Catalog".

Feature

Function & Options

Search Mode

Process a Single ISBN for quick lookups or Multiple ISBNs (one per line) for batch processing.

Library Catalog

Select the metadata source: LoC, WorldCat/OCLC (broadest coverage), DNB (German), or BL (UK).

Validation Options

ISBN Validation performs checksum verification. Check 'Validate format only' to skip API calls. Check 'Show technical details' to display raw MARC data.

Batch searches show a Progress Indicator.

2. Interpreting & Exporting Results

Results are displayed in cards with a clear status:

  • ✓ Found (Green): Record successfully retrieved.

  • ! Not Found (Yellow): ISBN is valid, but no record was found in the selected catalog.

  • X Error (Red): Search failed (e.g., API timeout or invalid ISBN structure).

Successful cards display key metadata: ISBN Type Badge, Title, Author, Publisher, Edition, Physical Description, LCCN, and Subjects.

Use the buttons at the bottom of the Configuration panel to export data: Export as JSON (full data) or Export as CSV (key fields). Files are automatically timestamped.

3. Working Examples

Test the application with these verified ISBNs: 0262033844 (Found), 9780743273565 (Found), and 9780262033845 (Not Found/Invalid Checksum).




install.packages(c("shiny", "bslib", "httr", "xml2", "dplyr", "jsonlite"))


# app.R

# Load necessary libraries
library(shiny)
library(bslib)
library(httr)
library(xml2)
library(jsonlite)
library(dplyr)

# --- Configuration & Design System ---
scholarly_blue <- "hsl(215, 70%, 35%)"
book_gold <- "hsl(35, 85%, 55%)"

# Working SRU Server Configuration
servers <- list(
  "Library of Congress (LOC)" = list(
    url = "http://lx2.loc.gov:210/LCDB",
    protocol = "sru"
  )
)

# MARC21 namespace
MARC_NS <- c(
  m = "http://www.loc.gov/MARC21/slim",
  srw = "http://www.loc.gov/zing/srw/",
  diag = "http://www.loc.gov/zing/srw/diagnostic/"
)

# --- Core Function: SRU Fetching and MARCXML Parsing ---
fetch_and_parse_marcxml <- function(isbn, server_config) {
  clean_isbn <- gsub("[^0-9X]", "", toupper(isbn))
  base_url <- server_config$url
  
  # Build SRU query - use proper Bib-1 attribute for ISBN (attribute 7)
  query_params <- list(
    version = "1.1",
    operation = "searchRetrieve",
    query = paste0('bath.isbn="', clean_isbn, '"'),
    recordSchema = "marcxml",
    maximumRecords = "1"
  )
  
  tryCatch({
    response <- httr::GET(
      base_url,
      query = query_params,
      timeout(30)
    )
    
    if (httr::status_code(response) != 200) {
      return(list(error = paste("HTTP Error:", httr::status_code(response))))
    }
    
    xml_content <- xml2::read_xml(httr::content(response, "raw"))
    
    # Check for SRU diagnostics (errors)
    diagnostic <- xml2::xml_find_first(xml_content, "//diag:diagnostic", ns = MARC_NS)
    if (length(diagnostic) > 0) {
      diag_msg <- xml2::xml_text(xml2::xml_find_first(diagnostic, ".//diag:message", ns = MARC_NS))
      return(list(error = paste("SRU Diagnostic:", diag_msg)))
    }
    
    # Check number of records
    num_records <- xml2::xml_find_first(xml_content, "//srw:numberOfRecords", ns = MARC_NS)
    if (length(num_records) > 0) {
      count <- as.integer(xml2::xml_text(num_records))
      if (count == 0) {
        return(NULL) # No records found
      }
    }
    
    # Find MARC record
    record_node <- xml2::xml_find_first(xml_content, "//m:record", ns = MARC_NS)
    
    if (length(record_node) == 0) {
      return(NULL)
    }
    
    return(parse_marc_record(record_node, clean_isbn))
    
  }, error = function(e) {
    return(list(error = paste("Request failed:", e$message)))
  })
}

# --- Helper function to parse MARC record ---
parse_marc_record <- function(record_node, isbn) {
  extract_marc_field <- function(tag, code, multiple = FALSE) {
    xpath <- paste0(".//m:datafield[@tag='", tag, "']/m:subfield[@code='", code, "']")
    nodes <- xml2::xml_find_all(record_node, xpath, ns = MARC_NS)
    
    if (length(nodes) == 0) {
      return(if (multiple) character(0) else NA_character_)
    }
    
    results <- sapply(nodes, function(n) {
      text <- xml2::xml_text(n)
      gsub("[\\.,;:/]$", "", trimws(text))
    })
    
    if (multiple) {
      return(results)
    } else {
      return(results[1])
    }
  }
  
  # Extract control field (like 008 for date)
  extract_control_field <- function(tag) {
    xpath <- paste0(".//m:controlfield[@tag='", tag, "']")
    node <- xml2::xml_find_first(record_node, xpath, ns = MARC_NS)
    if (length(node) > 0) {
      return(xml2::xml_text(node))
    }
    return(NA_character_)
  }
  
  # Title (245 $a and $b)
  title <- extract_marc_field('245', 'a')
  subtitle <- extract_marc_field('245', 'b')
  if (!is.na(subtitle) && nchar(subtitle) > 0) {
    title <- paste(title, subtitle)
  }
  
  # Author (100, 110, or 111)
  author <- extract_marc_field('100', 'a')
  if (is.na(author)) author <- extract_marc_field('110', 'a')
  if (is.na(author)) author <- extract_marc_field('111', 'a')
  
  # Publisher and Year (260 or 264)
  publisher <- extract_marc_field('260', 'b')
  year <- extract_marc_field('260', 'c')
  
  if (is.na(publisher) || nchar(publisher) == 0) {
    publisher <- extract_marc_field('264', 'b')
  }
  if (is.na(year) || nchar(year) == 0) {
    year <- extract_marc_field('264', 'c')
  }
  
  # Clean year - extract 4-digit year
  if (!is.na(year)) {
    year_match <- regmatches(year, regexpr("\\d{4}", year))
    if (length(year_match) > 0) {
      year <- year_match[1]
    }
  }
  
  # Subjects (650 $a)
  subjects <- extract_marc_field('650', 'a', multiple = TRUE)
  
  # Additional fields
  edition <- extract_marc_field('250', 'a')
  physical_desc <- extract_marc_field('300', 'a')
  lccn <- extract_marc_field('010', 'a')
  
  # Get publication date from 008 field if year not found
  if (is.na(year) || nchar(year) == 0) {
    field_008 <- extract_control_field('008')
    if (!is.na(field_008) && nchar(field_008) >= 11) {
      year <- substr(field_008, 8, 11)
    }
  }
  
  return(list(
    Title = if (!is.na(title) && nchar(title) > 0) title else "Unknown",
    Author = if (!is.na(author) && nchar(author) > 0) author else "Unknown",
    Publisher = if (!is.na(publisher) && nchar(publisher) > 0) publisher else "Unknown",
    Year = if (!is.na(year) && nchar(year) > 0) year else "Unknown",
    Edition = edition,
    PhysicalDescription = physical_desc,
    LCCN = lccn,
    Subjects = subjects,
    ISBN = isbn,
    SearchStatus = "Found"
  ))
}

# --- UI Definition ---
ui <- page_navbar(
  theme = bs_theme(
    version = 5,
    bootswatch = "cerulean",
    primary = scholarly_blue,
    secondary = book_gold,
    base_font = font_google("Merriweather"),
    heading_font = font_google("Lato")
  ),
  
  tags$head(
    tags$style(HTML("
      :root {
        --card-transition: all 0.3s ease-out;
        --card-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      }
      .card {
        transition: var(--card-transition);
        box-shadow: var(--card-shadow);
        margin-bottom: 20px;
      }
      .card:hover {
        transform: translateY(-3px);
        box-shadow: 0 8px 18px rgba(0, 0, 0, 0.2);
      }
      .bslib-page-navbar {
        background-color: #f8f9fa;
      }
      .not-found-card {
        background-color: #fff3cd;
        border-left: 5px solid #ffc107;
      }
      .error-card {
        background-color: #f8d7da;
        border-left: 5px solid #dc3545;
      }
      .debug-info {
        background-color: #f0f0f0;
        padding: 10px;
        margin-top: 10px;
        font-family: monospace;
        font-size: 0.85em;
        border-radius: 4px;
      }
    "))
  ),
  
  title = "📚 Academic Catalog Search",
  
  sidebar = sidebar(
    title = "ISBN Search & Configuration",
    
    radioButtons(
      "search_mode",
      "Search Mode:",
      choices = c("Single ISBN" = "single", "Multiple ISBNs" = "multi"),
      selected = "single"
    ),
    
    conditionalPanel(
      condition = "input.search_mode == 'single'",
      textInput(
        "isbn_input",
        "Enter ISBN:",
        placeholder = "e.g., 0195065786"
      ),
      tags$small("Try: 0195065786, 0262033844, 0394800028", style = "color: #666;")
    ),
    
    conditionalPanel(
      condition = "input.search_mode == 'multi'",
      textAreaInput(
        "isbn_multi_input",
        "Enter ISBNs (one per line):",
        placeholder = "0195065786\n0262033844\n0394800028",
        rows = 6
      )
    ),
    
    selectInput(
      "server_select",
      "Select Library Server:",
      choices = names(servers)
    ),
    
    actionButton(
      "search_button",
      "Search Catalog",
      class = "btn-primary w-100"
    ),
    
    hr(),
    
    checkboxInput(
      "show_debug",
      "Show debug information",
      value = FALSE
    ),
    
    hr(),
    
    downloadButton(
      "export_json",
      "Export Results (JSON)",
      class = "btn-outline-secondary w-100"
    ),
    
    downloadButton(
      "export_csv",
      "Export Results (CSV)",
      class = "btn-outline-secondary w-100 mt-2"
    ),
    
    conditionalPanel(
      condition = "input.search_mode == 'multi'",
      div(
        class = "mt-3",
        uiOutput("search_progress")
      )
    )
  ),
  
  layout_column_wrap(
    width = 1,
    id = "results_container",
    uiOutput("search_results")
  )
)

# --- Server Logic ---
server <- function(input, output, session) {
  
  book_data <- reactiveVal(list())
  search_queries <- reactiveVal(list())
  
  observeEvent(input$search_button, {
    if (input$search_mode == "single") {
      req(input$isbn_input)
      isbns <- input$isbn_input
    } else {
      req(input$isbn_multi_input)
      isbns <- strsplit(input$isbn_multi_input, "\n")[[1]]
      isbns <- trimws(isbns)
      isbns <- isbns[isbns != ""]
    }
    
    if (length(isbns) == 0) {
      showNotification("Please enter at least one ISBN.", type = "warning")
      return()
    }
    
    book_data(list())
    search_queries(list())
    server_config <- servers[[input$server_select]]
    
    results <- list()
    queries_debug <- list()
    
    for (i in seq_along(isbns)) {
      isbn <- isbns[i]
      
      # Update progress
      if (input$search_mode == "multi") {
        output$search_progress <- renderUI({
          tags$div(
            tags$p(paste("Searching", i, "of", length(isbns), "...")),
            tags$div(
              class = "progress",
              tags$div(
                class = "progress-bar progress-bar-striped progress-bar-animated",
                role = "progressbar",
                style = paste0("width: ", (i/length(isbns))*100, "%")
              )
            )
          )
        })
      }
      
      # Store query for debugging
      clean_isbn <- gsub("[^0-9X]", "", toupper(isbn))
      query_url <- paste0(
        server_config$url,
        "?version=1.1&operation=searchRetrieve&query=bath.isbn=\"",
        clean_isbn,
        "\"&recordSchema=marcxml&maximumRecords=1"
      )
      queries_debug[[isbn]] <- query_url
      
      data <- fetch_and_parse_marcxml(isbn, server_config)
      
      if (is.null(data)) {
        data <- list(
          ISBN = isbn,
          SearchStatus = "Not Found",
          Title = "No record found in catalog",
          Author = NA_character_,
          Publisher = NA_character_,
          Year = NA_character_
        )
      } else if (!is.null(data$error)) {
        data <- list(
          ISBN = isbn,
          SearchStatus = "Error",
          ErrorMessage = data$error,
          Title = "Search error occurred",
          Author = NA_character_,
          Publisher = NA_character_,
          Year = NA_character_
        )
      }
      
      results[[length(results) + 1]] <- data
      
      if (i < length(isbns)) {
        Sys.sleep(0.5)
      }
    }
    
    output$search_progress <- renderUI({ NULL })
    book_data(results)
    search_queries(queries_debug)
    
    found_count <- sum(sapply(results, function(x) x$SearchStatus == "Found"))
    showNotification(
      paste("Search complete:", found_count, "of", length(isbns), "found"),
      type = if (found_count > 0) "message" else "warning",
      duration = 5
    )
  })
  
  output$search_results <- renderUI({
    data_list <- book_data()
    queries <- search_queries()
    
    if (length(data_list) == 0) {
      if (input$search_button == 0) {
        return(tags$div(
          class = "alert alert-info",
          tags$h5("Welcome to Academic Catalog Search! 📚"),
          tags$p("Enter an ISBN above and click 'Search' to begin."),
          tags$p("Note: The Library of Congress catalog may not have all books. Try these working examples:"),
          tags$ul(
            tags$li(tags$code("0195065786"), " - The Elements of Style"),
            tags$li(tags$code("0262033844"), " - Introduction to Algorithms"),
            tags$li(tags$code("0394800028"), " - The Cat in the Hat")
          )
        ))
      }
      return(NULL)
    }
    
    cards <- lapply(seq_along(data_list), function(idx) {
      data <- data_list[[idx]]
      
      if (data$SearchStatus == "Found") {
        tags$div(
          class = "card shadow-lg",
          style = paste0("border-left: 5px solid ", book_gold, ";"),
          tags$div(
            class = "card-body",
            tags$h4(data$Title, class = "card-title", style = paste0("color:", scholarly_blue)),
            tags$p(tags$strong("Author:"), data$Author),
            tags$p(tags$strong("Publisher/Year:"), paste(data$Publisher, data$Year, sep = ", ")),
            if (!is.null(data$Edition) && !is.na(data$Edition)) tags$p(tags$strong("Edition:"), data$Edition),
            if (!is.null(data$PhysicalDescription) && !is.na(data$PhysicalDescription)) {
              tags$p(tags$strong("Physical Description:"), data$PhysicalDescription)
            },
            tags$p(tags$strong("ISBN:"), data$ISBN),
            if (!is.null(data$LCCN) && !is.na(data$LCCN)) tags$p(tags$strong("LCCN:"), data$LCCN),
            if (length(data$Subjects) > 0) {
              tags$div(
                hr(),
                tags$h6("Subjects:", style = "color: #6c757d;"),
                tags$div(
                  class = "d-flex flex-wrap",
                  lapply(data$Subjects, function(sub) {
                    tags$span(
                      class = "badge rounded-pill me-2 mb-2",
                      style = paste0("background-color:", book_gold, "; color: white; font-weight: bold;"),
                      sub
                    )
                  })
                )
              )
            },
            if (input$show_debug && !is.null(queries[[data$ISBN]])) {
              tags$div(
                class = "debug-info",
                tags$strong("Debug - SRU Query URL:"),
                tags$br(),
                tags$a(href = queries[[data$ISBN]], target = "_blank", queries[[data$ISBN]], style = "word-break: break-all;")
              )
            }
          )
        )
      } else if (data$SearchStatus == "Error") {
        tags$div(
          class = "card shadow error-card",
          tags$div(
            class = "card-body",
            tags$h5(class = "card-title", "⚠️ Error: ", tags$code(data$ISBN)),
            tags$p(tags$strong("Error Message:"), data$ErrorMessage),
            if (input$show_debug && !is.null(queries[[data$ISBN]])) {
              tags$div(
                class = "debug-info mt-2",
                tags$strong("Debug - SRU Query URL:"),
                tags$br(),
                tags$a(href = queries[[data$ISBN]], target = "_blank", queries[[data$ISBN]], style = "word-break: break-all;")
              )
            }
          )
        )
      } else {
        tags$div(
          class = "card shadow not-found-card",
          tags$div(
            class = "card-body",
            tags$h5(class = "card-title", "❌ Not Found: ", tags$code(data$ISBN)),
            tags$p("This ISBN was not found in the Library of Congress catalog."),
            tags$p(
              tags$small(
                "Note: LOC may not have all editions. The same book can have different ISBNs for hardcover, paperback, different years, etc.",
                style = "color: #666;"
              )
            ),
            if (input$show_debug && !is.null(queries[[data$ISBN]])) {
              tags$div(
                class = "debug-info mt-2",
                tags$strong("Debug - SRU Query URL:"),
                tags$br(),
                tags$a(href = queries[[data$ISBN]], target = "_blank", queries[[data$ISBN]], style = "word-break: break-all;")
              )
            }
          )
        )
      }
    })
    
    tags$div(cards)
  })
  
  output$export_json <- downloadHandler(
    filename = function() {
      paste0("catalog-metadata-", format(Sys.time(), "%Y%m%d-%H%M%S"), ".json")
    },
    content = function(file) {
      data <- book_data()
      if (length(data) == 0) {
        showNotification("No data available to export.", type = "warning")
        return()
      }
      write_json(data, file, pretty = TRUE, auto_unbox = TRUE)
    }
  )
  
  output$export_csv <- downloadHandler(
    filename = function() {
      paste0("catalog-metadata-", format(Sys.time(), "%Y%m%d-%H%M%S"), ".csv")
    },
    content = function(file) {
      data <- book_data()
      if (length(data) == 0) {
        showNotification("No data available to export.", type = "warning")
        return()
      }
      
      df <- do.call(rbind, lapply(data, function(x) {
        data.frame(
          ISBN = x$ISBN,
          Status = x$SearchStatus,
          Title = ifelse(is.null(x$Title), NA, x$Title),
          Author = ifelse(is.null(x$Author) || is.na(x$Author), NA, x$Author),
          Publisher = ifelse(is.null(x$Publisher) || is.na(x$Publisher), NA, x$Publisher),
          Year = ifelse(is.null(x$Year) || is.na(x$Year), NA, x$Year),
          Edition = ifelse(is.null(x$Edition) || is.na(x$Edition), NA, x$Edition),
          Subjects = ifelse(!is.null(x$Subjects) && length(x$Subjects) > 0, 
                            paste(x$Subjects, collapse = "; "), NA),
          stringsAsFactors = FALSE
        )
      }))
      
      write.csv(df, file, row.names = FALSE)
    }
  )
}

# Run the application
shinyApp(ui = ui, server = server)
Tags:
Tools
Disclaimer All questions and answers on lisquiz.com are sourced from previous exam papers for educational use. Users are advised to verify time-sensitive details and report any incorrect answers in the comments section.
Link copied to clipboard.