FTS Indexing Example
Index PDF assets and search through them using PdfLibrary and LibraryFileSystemDataSource.
/* * Copyright © 2025-2026 PSPDFKit GmbH. All rights reserved. * * The PSPDFKit Sample applications are licensed with a modified BSD license. * Please see License for details. This notice may not be removed from this file. */
package com.pspdfkit.catalog.examples.kotlin
import android.content.Contextimport android.content.Intentimport android.net.Uriimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.material3.Cardimport androidx.compose.material3.CardDefaultsimport androidx.compose.material3.CircularProgressIndicatorimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.OutlinedTextFieldimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.DisposableEffectimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.ui.theming.CatalogThemeimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.document.library.LibraryFileSystemDataSourceimport com.pspdfkit.document.library.PdfLibraryimport com.pspdfkit.document.library.QueryOptionsimport com.pspdfkit.document.library.QueryPreviewResultimport com.pspdfkit.document.library.QueryResultListenerimport com.pspdfkit.ui.PdfActivityIntentBuilderimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.withContextimport java.io.File
class IndexedFullTextSearchExample(context: Context) : SdkExample( context, R.string.indexedFtsExampleTitle, R.string.indexedFtsExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { val intent = Intent(context, IndexedFullTextSearchActivity::class.java) context.startActivity(intent) }}
class IndexedFullTextSearchActivity : ComponentActivity() {
companion object { private const val EXTRACTED_PDFS_DIR = "extracted_pdfs" }
private var pdfLibrary: PdfLibrary? = null private var dataSource: LibraryFileSystemDataSource? = null
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
setContent { CatalogTheme { LibrarySearchScreen() } } }
override fun onDestroy() { super.onDestroy() dataSource?.cleanup() pdfLibrary = null dataSource = null }
@Composable private fun LibrarySearchScreen() { var searchQuery by remember { mutableStateOf("") } var searchResults by remember { mutableStateOf<Map<String, Set<QueryPreviewResult>>>(emptyMap()) } var isIndexing by remember { mutableStateOf(true) } var indexingProgress by remember { mutableStateOf("Preparing to index...") } var extractedDirectory by remember { mutableStateOf<File?>(null) }
LaunchedEffect(Unit) { withContext(Dispatchers.IO) { try { extractedDirectory = extractAllPdfAssets() initializePdfLibrary(extractedDirectory!!) { progress -> indexingProgress = progress if (progress.contains("completed")) { isIndexing = false } } } catch (e: Exception) { indexingProgress = "Error: ${e.message}" isIndexing = false } } }
LaunchedEffect(searchQuery, isIndexing) { if (searchQuery.isNotEmpty() && pdfLibrary != null && !isIndexing) { performSearch(searchQuery) { results -> searchResults = results } } else if (searchQuery.isEmpty()) { searchResults = emptyMap() } }
DisposableEffect(Unit) { onDispose { dataSource?.cleanup() } }
Scaffold( modifier = Modifier.fillMaxSize() ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) .padding(16.dp) ) { OutlinedTextField( value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Search PDFs") }, placeholder = { Text("Enter search terms...") }, modifier = Modifier.fillMaxWidth(), enabled = !isIndexing )
if (isIndexing) { Column( modifier = Modifier .fillMaxWidth() .padding(vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() Text( text = indexingProgress, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp) ) } } else if (searchQuery.isEmpty()) { Text( text = "Enter a search term to find content in the PDF library", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(vertical = 16.dp) ) } else if (searchResults.isEmpty()) { Text( text = "No results found for \"$searchQuery\"", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(vertical = 16.dp) ) }
LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp) ) { searchResults.forEach { (_, results) -> items(results.toList()) { result -> SearchResultCard( result = result, onResultClick = { clickedResult -> openDocumentAtPage(clickedResult) } ) } } } } } }
@Composable private fun SearchResultCard( result: QueryPreviewResult, onResultClick: (QueryPreviewResult) -> Unit ) { Card( modifier = Modifier .fillMaxWidth() .clickable { onResultClick(result) }, elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column( modifier = Modifier.padding(12.dp) ) { Text( text = "Document: ${getDocumentName(result.uid)}", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Text( text = "Page ${result.pageIndex + 1}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(bottom = 4.dp) ) Text( text = result.previewText, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 4.dp) ) } } }
private suspend fun extractAllPdfAssets(): File { return withContext(Dispatchers.IO) { val extractedDir = File(filesDir, EXTRACTED_PDFS_DIR) if (!extractedDir.exists()) { extractedDir.mkdirs() }
val assetManager = assets val pdfAssets = assetManager.list("")?.filter { it.endsWith(".pdf") } ?: emptyList()
for (assetName in pdfAssets) { val outputFile = File(extractedDir, assetName) if (!outputFile.exists()) { assetManager.open(assetName).use { input -> outputFile.outputStream().use { output -> input.copyTo(output) } } } }
extractedDir } }
private suspend fun initializePdfLibrary( documentsDirectory: File, onProgress: (String) -> Unit ) { withContext(Dispatchers.IO) { try { onProgress("Initializing PDF library...")
val libraryDbPath = File(filesDir, "pdf_library.db").absolutePath pdfLibrary = PdfLibrary(libraryDbPath) val library = pdfLibrary ?: throw IllegalStateException("Failed to create PdfLibrary instance") onProgress("Setting up data source...") dataSource = LibraryFileSystemDataSource(library, documentsDirectory) library.dataSource = dataSource
onProgress("Indexing...") library.updateIndexFromDataSource()
onProgress("Indexing completed!") } catch (e: Exception) { onProgress("Error during initialization: ${e.message}") throw e } } }
private fun performSearch( query: String, onResults: (Map<String, Set<QueryPreviewResult>>) -> Unit ) { val library = pdfLibrary ?: return
val queryOptions = QueryOptions.Builder() .generateTextPreviews(true) .maximumPreviewResultsPerDocument(3) .maximumPreviewResultsTotal(20) .build()
library.search( query, queryOptions, object : QueryResultListener { override fun onSearchCompleted(searchString: String, results: Map<String, Set<Int>>) { // Basic results handled by preview callback }
override fun onSearchPreviewsGenerated( searchString: String, results: Map<String, Set<QueryPreviewResult>> ) { runOnUiThread { onResults(results) } } } ) }
private fun openDocumentAtPage(result: QueryPreviewResult) { val descriptor = dataSource?.indexItemDescriptorForDocumentWithUid(result.uid) if (descriptor != null) { try { // Construct the full path to the extracted file val extractedDir = File(filesDir, EXTRACTED_PDFS_DIR) val documentFile = File(extractedDir, descriptor.documentPath) val fileUri = Uri.fromFile(documentFile)
val configuration = PdfActivityConfiguration.Builder(this) .page(result.pageIndex) .build()
val intent = PdfActivityIntentBuilder.fromUri(this, fileUri) .configuration(configuration) .build()
startActivity(intent) } catch (e: Exception) { // Handle error - could show a toast or log android.util.Log.e("LibraryAssetSearch", "Error opening document: ${e.message}") } } }
private fun getDocumentName(uid: String): String { val descriptor = dataSource?.indexItemDescriptorForDocumentWithUid(uid) return descriptor?.documentPath?.substringAfterLast("/") ?: "Unknown Document" }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.